Skip to content
AutoSuggesAutoSugges home
Start free
Menu
Appearance
Appearance: System.

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 by displayOrder then slug. Paginated.
  • POST /lists — create a list from { name, description? }. Accepts Idempotency-Key. 201 with an ETag header.
  • GET /lists/{id} — one list, with an ETag header.
  • 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 new ETag, plus edgeSync when visibility, status or query policy changed.
  • GET /lists/{id}/items — paginated, ordered by displayOrder then id.
  • POST /lists/{id}/items — add an item. Accepts Idempotency-Key. 201.
  • GET /lists/{id}/versions — paginated, newest first.
  • GET /lists/{id}/versions/{versionNumber} — one version. Staged, active and superseded versions are immutable; their ETag is the version’s contentHash. A number the list never allocated is 400 invalid_request, field version.
  • POST /lists/{id}/versions — publish: validate, compile, activate. Accepts Idempotency-Key. 200 once resolved (active, or rejected back to draft with validationErrors), 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. Never 403, even when the list exists.
  • 400 invalid_request — malformed body, query parameter or header; details.fields names each field.
  • 412 invalid_request — an If-Match precondition failed. See Optimistic concurrency.
  • 422 invalid_request / 409 invalid_request — an Idempotency-Key conflict. See Idempotency.
  • 429 rate_limited — 60 requests per minute per key or 120 per minute per IP exceeded; retry after the retry-after header.

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.

Call the control-plane API with a secret key — AutoSugges