Skip to content

Authentication ​

Every public API request is authenticated with an API key. A key belongs to exactly one organization, and that organization is the only tenant a request can act as — there is no parameter, header or body field that changes it.

Creating a key ​

Keys are minted in the dashboard under Settings → API keys. You choose the key's name and its scopes at creation time.

The key is shown once, at creation. Store it in your own secret manager before closing the dialog; we keep only a hash and cannot show it again.

Sending a key ​

The documented way in is the Authorization header with the Bearer scheme:

authorization: Bearer <key>

The scheme is compared case-insensitively, per RFC 9110 §11.1, so bearer works.

An alternative header is accepted:

x-api-key: <key>

This is not deprecated and not second-class — enough hosted HTTP clients, gateways and low-code platforms mangle or strip Authorization that refusing the alternative would buy nothing but support tickets. Use whichever survives your stack.

Both header names are defined in API.HEADER in src/lib/const/api.const.ts.

Verifying a key works ​

Call the root endpoint, which requires no scopes:

bash
curl -sS https://<your-app-domain>/api/public/v1 \
  -H "authorization: Bearer $CALL_CENTER_API_KEY"
json
{
  "data": {
    "version": "v1",
    "organization_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "key": { "id": "k_…", "name": "Acme CRM sync" },
    "scopes": ["contact:read", "contact:write"]
  }
}

This answers the three questions a failing integration actually has — did the key authenticate, which organization does it speak for, and which scopes did it end up with — without you having to guess whether an empty list means "no records" or "wrong tenant".

When authentication fails ​

A missing or malformed credential is a 401 with code UNAUTHORIZED:

json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Provide an API key as `authorization: Bearer <key>` or `x-api-key: <key>`."
  }
}

You will also see 401 with "This API key's organization no longer exists." if the organization behind the key has been deleted.

A key that authenticated but is not permitted to do this particular thing is a 403 — see Scopes. The distinction matters when you are debugging: 401 means we do not know who you are, 403 means we do and the answer is no.

Failed authentication is rate limited separately and more tightly than successful traffic, by IP. A client looping on a bad key will start receiving 429 — see Rate limits.

Request identifiers ​

Send x-request-id with a value you generate (a UUID is ideal). It is returned on the response, appears in our logs, and is the fastest way to have a specific request looked at.

If you do not send one, we mint one and return it anyway — so capture the response header either way.

Transport and origin rules ​

  • HTTPS only.
  • Server-to-server only. There is no CORS configuration and no OPTIONS handler, deliberately: a key usable from a browser is a key published in client-side source. Calling this API from front-end JavaScript will fail, and that is the intended behaviour.
  • An unsupported HTTP method on a real route returns 405 with an allow header, in the same JSON envelope as every other error.

Rotation ​

Create the replacement key first, deploy it, confirm traffic on the new key id via the root endpoint, then delete the old one. Deleting a key takes effect immediately and in-flight requests holding it will start failing with 401.