Call the control-plane API with a secret key
Read and change your workspace’s lists from a server with a secret key: cursor pagination, idempotency keys and If-Match concurrency.
When you need it
This is the API for managing lists and items from your own backend or a script — a provisioning job, a CI step, a sync from your own system of record. It is entirely separate from the runtime autocomplete API: that one is public, unauthenticated beyond a publishable key, and read-only; this one authenticates with a secret key and can create and change data.
Authentication
Every request carries Authorization: Bearer $AUTOSUGGES_SECRET_KEY. Issue the key once from Dashboard → Applications — it is shown exactly once. This API has no CORS policy and is never called from a browser or mobile app.
A secret key is a tenant-level credential: it can read, edit and publish every list your workspace owns (not lists it only subscribes to), with the rights of the data_steward role — list content and publishing, never keys, members, domains or billing. It is not tied to the person who issued it. Treat it like a root password, and revoke it from the dashboard the moment it may have leaked.
A missing, wrong or revoked key, or a disabled workspace, answers 401 invalid_key with a WWW-Authenticate: Bearer header. This route shares its rate limit with POST /api/v1/token: 60 requests per minute per key and 120 per minute per IP, over which the answer is 429 rate_limited with a retry-after: 60 header.
Pagination
Every collection GET takes ?limit= (1–100, default 50 — outside that range is 400 invalid_request naming field limit) and ?cursor=, an opaque string copied verbatim from the previous response’s nextCursor (a malformed cursor is 400 invalid_request naming field cursor). The body is { data, nextCursor, requestId }; keep paging while nextCursor is non-null.
Idempotency
The three write endpoints below — creating a list, adding an item, publishing a version — accept an optional Idempotency-Key header (1–255 printable ASCII characters), remembered per workspace for 24 hours. The identical key on the identical method, path and body replays the original status and body with an Idempotent-Replayed: true header instead of re-executing. The same key with a different body is 422 invalid_request (details.reason: "idempotency_key_reused"); the same key while the first call is still running is 409 invalid_request (details.reason: "idempotency_key_in_flight") — retry after one second. A 5xx or 429 answer is never remembered: retry the same key freely after one of those.
Optimistic concurrency
PATCH /lists/{id} accepts an optional If-Match header — an ETag value from a prior response, or *. A mismatch answers 412 with body code invalid_request, details.reason: "precondition_failed", and the list’s current ETag header, rather than silently applying your change over someone else’s.
Honestly: the check is compare-then-write in the API layer, not a database-level conditional update, so two writers racing within the same few milliseconds can both pass it. Send If-Match anyway — it catches every realistic lost update, which is nearly all of them; it is not a distributed lock.
Endpoints
GET /lists— lists your workspace owns, ordered bydisplayOrderthenslug. Paginated.POST /lists— create a list from{ name, description? }. AcceptsIdempotency-Key. 201 with anETagheader.GET /lists/{id}— one list, with anETagheader.PATCH /lists/{id}— update any of{ name, visibility, status, queryPolicy, displayOrder }— the same fields the dashboard’s own list settings form writes. 200 with a newETag, plusedgeSyncwhen visibility, status or query policy changed.GET /lists/{id}/items— paginated, ordered bydisplayOrderthenid.POST /lists/{id}/items— add an item. AcceptsIdempotency-Key. 201.GET /lists/{id}/versions— paginated, newest first.GET /lists/{id}/versions/{versionNumber}— one version. Staged, active and superseded versions are immutable; theirETagis the version’scontentHash. A number the list never allocated is400 invalid_request, fieldversion.POST /lists/{id}/versions— publish: validate, compile, activate. AcceptsIdempotency-Key. 200 once resolved (active, or rejected back to draft withvalidationErrors), or 202 if queued — poll the version to see it resolve.
Example: list, then publish
# List every list your workspace owns
curl -sS 'https://staging.app.autosugges.com/api/v1/lists?limit=50' \
-H "Authorization: Bearer $AUTOSUGGES_SECRET_KEY"
# Create a list, guarded against a duplicate retry
curl -sS -X POST 'https://staging.app.autosugges.com/api/v1/lists' \
-H "Authorization: Bearer $AUTOSUGGES_SECRET_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: YOUR_IDEMPOTENCY_KEY' \
-d '{"name": "US Cities"}'
# Update it, only if it has not changed since you last read it
curl -sS -X PATCH 'https://staging.app.autosugges.com/api/v1/lists/YOUR_LIST_ID' \
-H "Authorization: Bearer $AUTOSUGGES_SECRET_KEY" \
-H 'Content-Type: application/json' \
-H 'If-Match: "YOUR_LAST_KNOWN_ETAG"' \
-d '{"visibility": "public"}'
# Publish a new version
curl -sS -X POST 'https://staging.app.autosugges.com/api/v1/lists/YOUR_LIST_ID/versions' \
-H "Authorization: Bearer $AUTOSUGGES_SECRET_KEY" \
-H 'Idempotency-Key: YOUR_IDEMPOTENCY_KEY'Example: server-side fetch
The same calls from a Node (or any Fetch-API) backend, reading the secret key from an environment variable your process never logs.
// Server-side only. AUTOSUGGES_SECRET_KEY must never reach a browser bundle.
const base = 'https://staging.app.autosugges.com/api/v1';
const headers = {
Authorization: `Bearer ${process.env.AUTOSUGGES_SECRET_KEY}`,
'Content-Type': 'application/json',
};
async function listOwnedLists() {
const response = await fetch(`${base}/lists?limit=50`, { headers });
if (!response.ok) {
const body = await response.json();
throw new Error(`${body.code}: ${body.message}`); // switch on body.code
}
const { data, nextCursor } = await response.json();
return { data, nextCursor }; // follow nextCursor until it is null
}
async function addItem(listId, item, idempotencyKey) {
const response = await fetch(`${base}/lists/${listId}/items`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(item),
});
if (response.status === 409) throw new Error('idempotency key still in flight, retry after 1s');
if (!response.ok) {
const body = await response.json();
throw new Error(`${body.code}: ${body.message}`);
}
return response.json();
}Errors
Every error body has the same shape (PRD §50):
{
"code": "invalid_request",
"message": "…",
"requestId": "…",
"details": { "fields": [{ "path": "limit", "reason": "…" }] },
"docsUrl": "…",
"remediation": "…"
}401 invalid_key— the secret key is missing, wrong, revoked, or its workspace is inactive.404 invalid_list— the list id is unknown or not yours. Never403, even when the list exists.400 invalid_request— malformed body, query parameter or header;details.fieldsnames each field.412 invalid_request— anIf-Matchprecondition failed. See Optimistic concurrency.422 invalid_request/409 invalid_request— anIdempotency-Keyconflict. See Idempotency.429 rate_limited— 60 requests per minute per key or 120 per minute per IP exceeded; retry after theretry-afterheader.
Send an x-request-id header of your own to correlate a call across your logs and AutoSugges’s; the response echoes it back, or generates one if you did not send it.
Versioning
Every route lives under /api/v1. Changes within v1 are additive only — a field already documented here is never renamed or removed.