API basics
Keys, errors, safe retries, usage, limits and lists.
Base URL: https://api.seekgist.com. Every path starts with /v1. The full list of endpoints is in the API reference.
Keys
| Credential | Looks like | Use it for |
|---|---|---|
| Public key | sg_pub_ + 44 characters | suggest, search, recommend, click, event; assistant chat only for a key made with Also allow the shopping assistant |
| Secret key | sg_sec_ + 44 characters | products, synonyms, rules, translation, stats, webhooks |
| Session | cookie sg_session | the dashboard only |
Send a key in one of these ways:
Authorization: Bearer sg_pub_...
X-SeekGist-Key: sg_pub_...
GET /v1/suggest?q=lab&key=sg_pub_... (public keys only)- A secret key in a URL is refused with
401. - You can lock a public key to your website with
allowed_origins. - Each key has scopes, for example
searchorcatalog:write. A missing scope gives403 insufficient_scope. GET /v1/whoamitells you which store and scopes a key has. Connectors use it for their "Test connection" button.
Errors
Every error has the same shape (RFC 9457, Content-Type: application/problem+json):
{
"type": "https://seekgist.com/docs/errors/invalid_request",
"title": "Invalid request",
"status": 422,
"code": "invalid_request",
"detail": "Some fields are not valid.",
"request_id": "req_4f1c0d9a8b7e6f5a4b3c",
"errors": [{ "field": "upsert.3.price", "code": "greater_than_equal", "message": "Input should be >= 0" }]
}Use code in your program: it never changes. Quote request_id when you ask for help.
| Code | Meaning |
|---|---|
invalid_request | Some fields are not valid. See errors. |
unauthorized | No key, or a wrong key. |
forbidden, insufficient_scope | The key may not do this. |
not_found | The thing does not exist (or belongs to another store). |
conflict | The change clashes with the current state. |
idempotency_key_reuse, idempotency_in_progress | See "Safe retries" below. |
rate_limited | Too many requests. Wait for Retry-After seconds. |
plan_limit_reached | Your plan limit is reached. |
unsupported_locale | The language is not one of your store's languages. |
not_implemented | This endpoint is not built yet. |
internal_error | Our fault. Retry later and quote request_id. |
Safe retries
Send Idempotency-Key: <a unique value, 8-255 characters> on writes (POST, PUT, PATCH, DELETE). If the network drops and you send the same request again with the same key, you get the first answer back with Idempotent-Replayed: true, and nothing happens twice.
- Keys are kept for 24 hours per store.
- The same key with a different body gives
422 idempotency_key_reuse. - The same key while the first call still runs gives
409 idempotency_in_progress.
Usage and billing
- Only searches are billed: a
/v1/searchcall withpage=1. Pages 2 and later are free. - Suggestions while typing are never billed.
- Every search answer has a header like this (
limit=-1means no fixed limit):
X-SeekGist-Usage: searches=1234, limit=150000, billed=1, period="2026-10"Rate limits
Limits are per key, per second. Every answer has RateLimit-Limit and RateLimit-Remaining. Over the limit you get 429 rate_limited with Retry-After.
Lists
Lists answer {"data": [...], "next_cursor": "...", "total": 123}. Pass next_cursor back as ?cursor= to get the next page. total is there when it is cheap to count.
Long jobs
Reindexing, imports, syncs, translation and exports answer 202 with a job. Call GET /v1/jobs/{id} until status is succeeded, failed or canceled.
Languages
lang, locale and target_locale take a two-letter code (en, ar). It must be one of your store's languages (GET /v1/settings shows locales), or you get 422 unsupported_locale. lang=auto lets SeekGist detect the language.
Webhooks
Webhooks are signed with the Standard Webhooks scheme. Check the signature before you trust a delivery:
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
key = base64.b64decode(secret.removeprefix("whsec_"))
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
expected = "v1," + base64.b64encode(mac).decode()
return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split())Reject timestamps more than 5 minutes from now, and drop a webhook-id you have seen before. Failed deliveries are retried with backoff for 3 days, then marked dead.
Versions
/v1 only grows: new endpoints, new optional fields, new values in answers. Your code must ignore fields it does not know. A breaking change would come as /v2.