On this page
Authentication and key hygiene
Every call to the API carries a session token, and every session token comes from an API key. This guide is the key's whole life: who mints it, what it may do, how it turns into a session, how you rotate it, and where it must never go.
Who mints a key
An API key belongs to your organization group and is minted by an owner of that group — in the office, under API keys, or by an owner's own code with mintApiKey. Mint one key per integration: your accountant's software, a connector and a script each get their own, so revoking one never breaks the others.
What a key may do
A key's scope is written as descriptor rows — the service, the action, allow or disallow — and it is fixed when the key is minted. A key can do no more than the owner who minted it, and it works on exactly the organizations of the group where that owner is currently an owner; if the owner loses ownership the key stops working, and it works again when ownership is granted back. To change a scope, mint a new key and retire the old one with doomApiKey: a scope is never edited in place.
Every key expires — at most 12 months after minting, on the date the owner chose. An expired key is a distinct, final state; it never comes back.
The secret shows once
Minting answers the key's secret exactly once. Store it in your integration's own secret store the moment you see it; the platform keeps only a hash and cannot show it again. Lose it and you mint a new key.
The exchange
Your code never sends the secret with its calls. It exchanges the secret for a session token with exchangeApiKey, naming the organization the session will act in. No token is needed for that one call — the secret is the credential.
Example request
mutation ExampleExchangeApiKey($input: ExchangeApiKeyInput!) {
exchangeApiKey(input: $input) {
token
expiresAt
}
}
Variables:
{
"input": {
"secret": "<your key secret>",
"orgId": "01900000-0000-7000-8000-b0cf4f3c0000"
}
}
Send it with the envelope naming the version: "extensions": {"at": {"version": {"name":"genesis","number":0}}}.
Example response
{
"data": {
"exchangeApiKey": {
"token": "<your session token>",
"expiresAt": "2027-01-31T00:00:00.000Z"
}
},
"extensions": {
"at": {
"callId": "01EXAMPLE-CALL-ID",
"version": {
"requested": {
"name": "genesis",
"number": 0
},
"serviced": {
"name": "genesis",
"number": 0
}
}
}
}
}
The session lasts 24 hours from the exchange and does not extend with use, so plan to exchange again before it ends. Every failed exchange answers the same way, whatever the cause: an unknown, expired or retired key, or an organization the key does not reach, all read as AUTHN/LOGIN_FAILED — nothing leaks about which keys exist. Repeated failed exchanges pause the exchange for a while, whether or not the key exists (Rate limits).
Calling with the token
Send the token on every call as an authorization: Bearer <your session token> header. A call with no token, or an expired one, answers AUTHN/REQUIRED; a call the key's scope does not allow answers AUTHZ/FORBIDDEN. Both are on the errors pages, and Errors and retries reads them.
Never a key in a browser
A key secret and a session token both grant your integration's whole scope to whoever holds them. Keep them on your servers: never in a web page, a mobile app or a shared spreadsheet, never in a URL, never in a log line. If a secret may have leaked, retire the key with doomApiKey and mint a new one.