Authentication
Every request carries an API key and its secret. The pair identifies a brand and a set of scopes; there is no user session and no cookie.
Creating a key
In the panel: System → API keys. Pick the scopes the integration actually needs and either set an expiry date or mark the key as never expiring.
The secret is shown once, at creation time. It is stored hashed, so it cannot be recovered — only rotated. Rotating changes the secret and keeps the key, so an integration only has to update one value.
Sending credentials
Three equivalent forms. Use the first unless something in your stack prevents it.
Authorization: Bearer <key>:<secret>
X-Api-Key: <key>
X-Api-Secret: <secret>
Authorization: Basic base64(<key>:<secret>)
What each rejection means
| Code | Meaning |
|---|---|
| authentication_required | Nothing was sent, or the header could not be parsed. |
| invalid_credentials | The key does not exist, or the secret does not match. The two cases are deliberately indistinguishable — a wrong key must not confirm that another key exists. |
| key_revoked | Someone switched the key off in the panel. Create a new one. |
| key_expired | Past its expiry date. The message names the exact date. |
| insufficient_scope | Credentials are fine; this key was not given the scope this endpoint needs. |
| plan_upgrade_required | API access starts with the Zincir plan. |
Brand isolation
A key can only ever see its own brand. Asking for a record that belongs to
someone else returns not_found, exactly as if the record did not
exist — the API never confirms that another brand's id is real.