Authentication
Status: Draft Last Updated: 2026-06-15 Audience: customer
This is the complete reference for the two token types you use to call the API: how to obtain each, how to send them, how to rotate them, and what every auth error means. If you are new, start with Quickstart; come here when you need the full picture.
All example credentials are synthetic. Replace them with your own, and never commit real secrets.
Overview: two token tiers
There are two token tiers, for two different jobs:
| Tier | Prefix | Scope | Expiry | Use for |
|---|---|---|---|---|
| End-user token | cct_ | One specific (your account, one end user) pair | None by default | Data queries: grants, messages, conversations, revokeGrant, etc. |
| Management token | ldb_ | Your entire account (all end users) | Always (1 hour) | Account visibility: authorizedEndUsers, endUserGrants |
Both are obtained from POST /oauth/token and both are sent as Authorization: Bearer … headers to POST /graphql/v1. The token you present determines what you can query.
End-user-scoped tokens (data access)
An end-user token authorizes data queries for one end user. You get one by sending that user through the consent flow and exchanging the resulting code.
- Issued by:
POST /oauth/tokenwithgrant_type=authorization_code. The full flow — building the/authorizeURL, the consent page, and receiving the code — is in Authorization Flow. - Scope: exactly one (your account, end user) pair. It can read only the sources that user has actively granted.
- Expiry: none by default. If your account is provisioned with a token TTL, the exchange response includes an
expires_infield and the token expires accordingly; otherwise it does not expire. - Format:
cct_<shortid>_<secret>.
Exchanging a code for an end-user token:
curl -X POST https://api.example-unbound.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=syn_authcode_5b2e8d1f4a6c0072" \ -d "client_id=a1b2c3d4" \ -d "client_secret=ldb_syn_clientsecret_REPLACE_ME"{ "access_token": "cct_syn_2a7b9c1d_K8mP3qR6sT0uV4wX7yZ1aB5cD9eF2gH", "token_type": "Bearer", "end_user_id": "user-42"}Store this token server-side, keyed by your own user record (the end_user_id echoed back is the identifier you supplied). Use it for every data request on that user’s behalf. See data-api.md for the queries it unlocks.
Management tokens (account-level access)
A management token gives you a read-only, account-wide view: which of your end users currently have active grants, and what those grants are. It never reads end-user message or data content.
- Issued by:
POST /oauth/tokenwithgrant_type=client_credentials. - Scope: your entire account — every end user under it.
- Expiry: always one hour (
expires_in: 3600). This is non-negotiable; management tokens are short-lived by design. - Getting a fresh one: simply call the endpoint again. Old tokens expire naturally — there is nothing to revoke.
- Format:
ldb_<shortid>_<secret>.
Minting a management token:
curl -X POST https://api.example-unbound.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=a1b2c3d4" \ -d "client_secret=ldb_syn_clientsecret_REPLACE_ME"{ "access_token": "ldb_syn_7h3k9m2p_X4f8Qa2bN6cR1dS5tU9vW0xY3zZ", "token_type": "Bearer", "expires_in": 3600}There is no end_user_id in the response — management tokens are bound to your account, not to any end user. The queries it unlocks are documented in management-api.md.
Both grants authenticate the same way: with your
client_idandclient_secret. You may send them in the form body (shown above) or via HTTP Basic auth. Theclient_credentialsgrant is enabled per account during onboarding; if it is not enabled, the endpoint returns{"error": "unsupported_grant_type"}.
Using bearer tokens
Send every request to POST /graphql/v1 with the token in the Authorization header:
Authorization: Bearer cct_syn_2a7b9c1d_K8mP3qR6sT0uV4wX7yZ1aB5cD9eF2gHcurl:
curl -X POST https://api.example-unbound.com/graphql/v1 \ -H "Authorization: Bearer cct_syn_2a7b9c1d_K8mP3qR6sT0uV4wX7yZ1aB5cD9eF2gH" \ -H "Content-Type: application/json" \ -d '{"query": "{ grants { source grantedAt } }"}'fetch (JavaScript):
await fetch("https://api.example-unbound.com/graphql/v1", { method: "POST", headers: { "Authorization": `Bearer ${endUserToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: "{ grants { source grantedAt } }" }),});GraphQL clients: set the Authorization: Bearer … header in the client’s HTTP-link configuration. The token tier you set determines which queries succeed.
Never put tokens in URL parameters or request bodies. Tokens belong only in the
Authorizationheader. URL parameters end up in logs, browser history, and referrer headers.
Auth error codes
Auth failures on POST /graphql/v1 return HTTP 401 with a JSON body carrying a machine-readable code:
{ "error": "invalid or missing api key", "code": "AUTH_INVALID_KEY" }| HTTP | code | Meaning | What to do |
|---|---|---|---|
401 | AUTH_INVALID_KEY | Token not found, malformed, secret wrong, or revoked. | Re-check the token. For an end-user token, re-run authorization; for a management token, re-mint. |
401 | AUTH_EXPIRED_KEY | The token has expired (a management token always expires after an hour). | Re-mint via client_credentials. |
Errors at POST /oauth/token follow the OAuth 2.0 convention — a JSON body with an error field:
| HTTP | error | Meaning | What to do |
|---|---|---|---|
401 / 400 | invalid_client | Wrong client_id or client_secret, or your credential is revoked/expired. The status reflects where you sent the credentials: 401 when presented via HTTP Basic auth, 400 when presented in the form body. | Verify your credentials from onboarding. |
400 | invalid_grant | Authorization code expired (10-minute limit) or already used. | Send the user through /authorize again. |
400 | invalid_request | A required parameter is missing or the request is malformed. | Check grant_type and required fields. |
400 | unsupported_grant_type | The grant type is not recognized, or client_credentials is not enabled for your account. | Use authorization_code or client_credentials; contact your operator to enable management tokens. |
A valid token presented to a query it is not authorized for (e.g. an end-user token calling a management query) is rejected at the GraphQL layer with an error whose extensions.code is UNAUTHENTICATED.
Empty results are not auth errors. When an end user has revoked all access, the end-user token stays valid and data queries return empty lists — not
401/403. This is deliberate: an auth error would leak grant state. To tell “no data yet” from “access revoked,” callgrantsfirst. Seedata-api.md.
Token storage recommendations
- End-user tokens: store server-side, associated with your own user record. Never expose them to a browser or mobile client.
- Management tokens: mint per request, or cache server-side for up to one hour. Never expose them to a client.
client_secret: server-side only. Never ship it to a browser or mobile app, and never commit it to source control.
Rotation
- End-user tokens: issuing a new token for a user does not invalidate the old one — multiple valid tokens per user are supported. To roll a token, mint a new one (re-run the authorization exchange) and switch your stored value; you do not need to coordinate a revoke.
- Management tokens: rotation is automatic. They expire after an hour; re-mint whenever you need a fresh one. There is nothing to revoke.
If you suspect a token is compromised, revoke the user’s access (see revokeGrant in data-api.md) or contact your Unbound operator to rotate your client credentials.
Related Documents
- Quickstart — Both tokens used end to end in sequence.
- Authorization Flow — How an end-user token is obtained via the consent flow.
- Data API — The data queries an end-user token unlocks, and the empty-result contract.
- Management API — The account-wide queries a management token unlocks.