Skip to main content

Authenticate with API keys

Authenticate Platform API requests with an X-Api-Key header, or a Console session on session-or-api-key routes, and store the secret when you create it.

Overview

An API key authenticates a script, backend service, or other direct integration with the Platform API. Each key you create belongs to one user and one organisation, carries a read or read-write scope, and can carry an expiry. A key created before the platform required an organisation can have none, as Organisation scope describes. Create one key per integration and give it only the access that integration needs. A key is a credential of its own: a password change, a ban, an account closure or an administrator's revoke stops it, but the end of the user's sessions does not, as What stops a key lists.

Send the key as the X-Api-Key header. The platform does not accept the key as a bearer token or a query parameter, so a client that copies a bearer-token pattern from another API fails authentication. Many Platform API routes, those whose OpenAPI operation carries x-dalp-auth: session-or-api-key, also accept a valid Console session cookie, so a browser client that already has a session does not need a long-lived key for those routes. When a request carries an X-Api-Key header, the platform authenticates it with the key alone: a key that fails leaves the request unauthenticated even if a valid session cookie came with it.

curl "https://your-instance.example.com/api/v2/tokens" \
  -H "X-Api-Key: sm_dalp_..."

Some routes accept only a signed-in session. A key on such a route receives DALP-0036, HTTP 403. The private file routes under /dalp/private/* ignore an API key and answer 401, the same answer as a request with no credential, as Files describes. Make those calls with a Console session instead.

Create a key

Create a key in the Console on API keys, run dalp login for the browser device-code flow, or call POST /api/auth/api-key/create. Install and set up the command-line tool explains the dalp login flow, which asks for your wallet code and creates a read-write key for the selected organisation that expires after 90 days. The /api/auth/* routes are unversioned. The key they create authenticates against the versioned /api/v2 Platform API. The v2 API reference lists the complete request and response for each /api/auth/api-key route.

The create response is the only time the full secret value appears. Store it when you receive it, because a later read of the same key returns metadata only.

A direct create call needs a signed-in session, and a request authenticated with an API key cannot create a key. A backend service that holds only a key therefore gets its keys from the Console or dalp login. The body carries:

  • name, required, 1 to 32 characters. Spaces at either end are trimmed before the check, and the key stores the trimmed name. A missing name, or one of only spaces, returns 400 NAME_REQUIRED and creates no key. The name is checked before walletVerification, so a refused name never uses up a recovery or authenticator code.
  • metadata, required, an object with the organizationId of an organisation you are a member of and an optional scope, such as {"organizationId": "<organisation id>", "scope": "read"}. GET /api/auth/organization/list returns the ids of your organisations. The scope is read or read-write, and a key with no scope is read-write. A call with no metadata, or with metadata that names no organisation, returns 400 API_KEY_METADATA_INVALID and creates no key. The platform checks metadata before walletVerification, so a call refused for its metadata never uses up a recovery or authenticator code.
  • expiresIn in seconds, at most 365 days. Omit it for a key that never expires. A longer value returns 400 EXPIRES_IN_IS_TOO_LARGE.
  • walletVerification, required, the wallet step-up that also guards signing: proof that the person creating the key controls the account's wallet. Send {"walletVerification": {"secretVerificationCode": "<code>", "verificationType": "PINCODE"}}, with PINCODE for the wallet PIN, OTP for a wallet authenticator code, OIDC_MFA for an identity-provider authenticator code or SECRET_CODES for a recovery code.

An account that holds an authenticator must send its code: a PIN or recovery code returns 400 WALLET_STEP_UP_STRONGER_FACTOR_REQUIRED. Without a proof the call returns 400 WALLET_STEP_UP_REQUIRED, or 401 WALLET_STEP_UP_UNAVAILABLE when the account has no wallet factor yet. The account password is not accepted in its place. During the cooling-off period after a wallet security change the call returns 403 WALLET_STEP_UP_HELD with heldUntil and creates no key. When the deployment sets enforce2fa, an account without two-factor sign-in gets 403 LOGIN_TWO_FACTOR_REQUIRED and no key, unless it signs in only through a configured identity provider with no password or passkey. A key would otherwise let the account reach the API without the second factor. Every new key emails the account holder.

The platform sets the rate-limit, request-budget and permission fields itself. A create or update call that sets rateLimitEnabled, rateLimitMax, rateLimitTimeWindow, remaining, refillAmount, refillInterval or permissions, or a create call that sets prefix, returns 400 SERVER_ONLY_PROPERTY.

A key is 54 characters: sm_dalp_, then a random label of 6 characters, then a random secret of 40 characters. Every character after the prefix is a letter a to z or A to Z, or a digit 0 to 9. The platform stores a digest of the full key, plus the key's first 14 characters in clear: the prefix and the label. The label is not part of the secret. GET /api/auth/api-key/list and GET /api/auth/api-key/get return those 14 characters to identify the key. A key issued before the label existed has a secret of 16 letters, keeps working, and is identified by the prefix alone.

Scope: read or read-write

Every key carries a scope of read or read-write. Every Platform API route declares whether it reads or writes. A read key can call routes declared as reads. On a route declared as a write, it receives DALP-0036 SESSION_POLICY_DENIED, HTTP 403, before the handler runs. The platform checks the route's declaration and ignores the HTTP method the request sends.

DALP-0036 is not a transient failure: retrying with the same key returns the same result. Issue a read-write key for that integration, or change the key's scope as Manage keys describes. A key stays bounded by its creator's permissions in the organisation. Changing a key from read to read-write does not give the creator a wider organisation role.

Organisation scope

A key carries the id of the organisation it was created for in its metadata. You can create a key only for an organisation you are a member of, and an update can move it only to another such organisation. Naming any other organisation returns 400 API_KEY_METADATA_INVALID and stores nothing. The platform reads the organisation from the key and scopes every request the key makes to it, so the caller does not repeat an organisation id on each request.

A key with no organisation is one whose metadata gives the platform no organisation to read: it is null or has no organizationId. Only a key created before the platform required an organisation can be one. Such a key works on the /api/auth routes that accept a key, where its only write is deleting itself. It is not held to one organisation there: on get-full-organization, list-members and has-permission it reads the organisation the request names, which can be any organisation its holder is a member of. A request that names no organisation gets null from get-full-organization and 400 NO_ACTIVE_ORGANIZATION from list-members and has-permission. On /api/v2 it does not authenticate: a route that needs a caller answers DALP-14386, HTTP 401. A key with no organisation keeps working when its holder leaves an organisation. A ban, an account closure or an administrator's revoke stops it, and a password change turns it off until its holder turns it on again, as What stops a key lists.

A key whose metadata names an organisation but has a scope other than read or read-write was created before the platform checked the scope. It has an organisation: on the /api/auth routes that accept a key it is held to the organisation its metadata names, like any other key, so naming another organisation on get-full-organization, list-members or has-permission answers 403 YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION, and GET /api/auth/organization/list and GET /api/auth/api-key/list return only that organisation and your keys for it. On /api/v2 it does not authenticate: a route that needs a caller answers DALP-14386, HTTP 401. Set its scope to read or read-write with an update, as Manage keys describes, to make it work on /api/v2.

The API keys screen in the Console does not list a key whose metadata names no organisation, and it lists one with an unknown scope under the organisation its metadata names, as Read. Its holder finds a key with no organisation with GET /api/auth/api-key/list from a signed-in session, which returns its metadata as null or without an organizationId. The holder deletes it with POST /api/auth/api-key/delete, from that session or from the key itself with its id as keyId. To keep an integration that uses it working, create a key for the integration's organisation and move the integration to the new key before you delete the old one, as Store and rotate a key describes. On Platform users, a platform administrator sees it in the holder's “API keys” section with a dash under Organisation, and a key with an unknown scope shows a dash there too. Revoke all API keys there deletes it with the holder's other keys.

API keys and delegated authority

An API key identifies the caller. Delegated Authority lets an entitled caller act in another participant's context. An API-key request can send X-Participant, the header that names the participant to act for, when the key's user holds the required authority. The key does not grant that authority by itself.

Without that header, an API-key request uses the acting participant selected for its user in the key's organisation: the person, organisation or account record whose authority the user acts with. After you select another acting participant, requests with the same key use that selection within 30 seconds.

What stops a key

A key is a credential of its own, separate from the user's sign-in. Most actions that end a person's sessions leave the person's keys working. Each Platform API server keeps a key's resolved identity for up to 30 seconds, so a change that stops a key can take that long to reach every server, or 35 seconds for a change to the holder's memberships.

Event
Effect on the user's keys
The key's expiry passes
The key stops authenticating at that instant.
The holder turns the key off or deletes it
The key stops authenticating within 30 seconds.
A password changes: the holder's own, an emailed-link reset, or one a platform administrator sets
Every key the user holds, in every organisation, is turned off before the new password is stored: its enabled field becomes false. The holder can enable each one again. When the platform cannot reach the key store to turn them off, it refuses the change with 503, a platform error you can retry, and the old password stays in force.
The holder leaves the key's organisation, or is removed from it
The key stops authenticating within 35 seconds. Its record stays unchanged. A key with no organisation is not stopped: it keeps authenticating on the /api/auth routes that accept a key, and reads only the organisations its holder still belongs to.
A wallet security change starts the holder's cooling-off period
The key keeps authenticating. A request that would need wallet verification returns 403 WALLET_VERIFICATION_HELD until the period ends.
A platform administrator bans the holder
The key stops authenticating within 30 seconds: /api/v2 answers 401, and the /api/auth routes that accept a key answer 401 KEY_DISABLED. It works again within 30 seconds of the ban being lifted or a timed ban ending. Its record stays unchanged.
A platform administrator closes the holder's account
The key stops authenticating within 30 seconds, with the same 401 answers as a ban. A closed account stays closed, so the key does not work again.
A platform administrator revokes the holder's keys
Every key the user holds, in every organisation, is deleted and stops authenticating within 30 seconds. It cannot be turned on again. The administrator uses Revoke all API keys on the user's page under Platform users, which calls POST /api/auth/admin/revoke-user-api-keys with the user's id.
A platform administrator ends the holder's sessions
No effect. The key keeps authenticating. For a password the administrator sets, see the password change row.

A write that a key submits for maker-checker approval runs as the key's holder once it is approved. If by then the key is revoked, turned off, expired or read-only, or its holder is banned or no longer a member of the organisation, the approved write does not run.

Manage keys

Operation
Route
Notes
List
GET /api/auth/api-key/list
Returns metadata for your keys. Secret values are never included.
Read one
GET /api/auth/api-key/get
Returns metadata for the key named by id: expiry, enabled, rate-limit settings, request count and last request time.
Update
POST /api/auth/api-key/update
Changes the name, the metadata with its scope and organisation, the expiry, or enabled. Read the returned record to confirm the change.
Delete
POST /api/auth/api-key/delete
Deletes the key. It stops authenticating within 30 seconds. Issue a new key if the integration still needs access.

Create and update need a signed-in session. List and read one also accept an API key. An update that only renames a key or sets enabled to false needs nothing more, even during the cooling-off period. Any other update, such as enabling a key or changing its scope, organisation or expiry, needs the same walletVerification block as create, is refused with the same errors, and emails the account holder. An update that sends no metadata keeps the key's organisation and scope. An update can set a new expiry of up to 365 days from the update, or null to remove the expiry. It can also enable a key again. Extend a key before it expires: the platform deletes the records of expired keys as keys are listed, read, created, updated or deleted, so an expired key soon has no record to update. For a key that has already expired, issue a new key.

Delete accepts a signed-in session, which can delete any key its user holds, or an API key that names itself as keyId. An API key that names any other key gets 403 API_KEY_DELETES_ONLY_ITSELF, and that other key keeps working. No user can delete another user's key.

Turning a key off is the reversible way to retire it. The record survives and the secret stops working, and enabling it again restores the same key, so the integration resumes without a redeploy. Deleting is permanent, and the right step once you know the secret has leaked.

The server that handles the change drops its copy of the key's identity at once, and every other server reads the record again when its copy runs out, so nothing has to be restarted for the cut-off to hold. A key turned off or deleted fails with DALP-14386 no later than 30 seconds after the change lands.

Read a key's own record before you conclude that an integration lost its secret. The caller sees the same DALP-14386 when a key is expired, turned off or unknown, has no organisation or an unknown scope, or has a holder who left its organisation. A turned-off key's record shows enabled: false, and a key with no organisation shows metadata that is null or has no organizationId, and a key with an unknown scope shows a scope other than read or read-write. An expired key's record is deleted soon after it expires, so a key that GET /api/auth/api-key/list does not return has expired or been deleted.

API keys on /api/auth routes

The /api/auth routes accept an API key on these nine routes only:

  • GET /api/auth/get-session
  • GET /api/auth/organization/list
  • GET /api/auth/organization/get-full-organization
  • GET /api/auth/organization/list-members
  • POST /api/auth/organization/has-permission
  • GET /api/auth/api-key/list
  • GET /api/auth/api-key/get
  • POST /api/auth/api-key/delete, for the presenting key only
  • GET /api/auth/passkey/list-user-passkeys

A key reads only the organisation it was created for on these routes. GET /api/auth/organization/list returns that one organisation, and GET /api/auth/api-key/list returns only your keys for that organisation, with total, limit and offset counting that list. A signed-in session still lists all of your organisations and keys.

A key on any other /api/auth route receives API_KEY_NOT_SUPPORTED_ON_ROUTE, HTTP 403, before the route runs. The refusal covers routes that need no sign-in, POST /api/auth/sign-out, and any path the v2 API reference does not list. The same request with a signed-in Console session still works. A key therefore cannot create or update a key, approve a device sign-in, register a passkey or sign in. The API reference lists eight of the nine routes. It leaves out GET /api/auth/organization/list-members, which accepts a key, because GET /api/v2/users lists the same members.

On get-full-organization, list-members and has-permission, a key with an organisation reads only that organisation. A request that names another organisation by organizationId, or by organizationSlug on the two GET routes, receives YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION, HTTP 403, even when the key's holder is a member of that organisation. A request that names no organisation reads the key's organisation. A key with no organisation reads any organisation its holder is a member of that the request names, as Organisation scope describes.

Rate limit

A key that the platform creates is not rate limited. Each key stores whether its limit is on, the length of its window, and how many requests that window admits. A key created through the Console, dalp login or POST /api/auth/api-key/create stores a 60-second window that admits 10,000 requests, with the limit off. No API call switches the limit on, so limit an integration by its scope and expiry instead. Only a key whose record has the limit on is counted. Read the record with GET /api/auth/api-key/get: rateLimitEnabled says whether the limit is on, rateLimitMax and rateLimitTimeWindow give the budget and the window in milliseconds, lastRequest shows the last accepted request, and requestCount counts the requests in the current window only while the limit is on.

The count belongs to that key alone. Separate keys for separate integrations get separate budgets rather than sharing one ceiling across the organisation or the user.

The count resets only after a full window passes with no accepted request, so continuous traffic does not receive a fresh budget every minute. A refused request is not counted and does not extend the window.

A request over the key's limit receives HTTP 429 with this body:

{
  "code": "auth.apikey.THROTTLED",
  "status": 429,
  "message": "This API key filled its request window",
  "data": {
    "id": "DALP-12136",
    "why": "This key used its full request allowance for the current window, so the platform did not authenticate the request.",
    "fix": "Wait for the key's request window to reopen, then send the request again.",
    "retryable": false,
    "category": "permission"
  }
}

The body sets retryable to false because an immediate retry meets the same refusal, not because the key is spent. The response carries no Retry-After header. Stop sending requests with that key, then retry once the window has reopened. In the key's record, lastRequest plus rateLimitTimeWindow marks when that happens.

The platform counts requests in memory on each API server and writes the count to the database in batches, so a request does not wait for a database write. With a limit of N requests, a key admits at most N requests plus 50 for each server beyond the first. Each server can admit up to 50 requests beyond its share before it reconciles with the stored count, and a single server enforces N exactly.

The key's last-used time updates within about a second of an accepted request. It records when the platform wrote the batch, so it can trail the request by up to about a second.

Store and rotate a key

Store the secret value in a secrets manager or environment variable, not in source control or a shared document. Rotate a key by creating a replacement, updating your integration to use it, then deleting the old key once the new one works. The platform does not roll a key's secret value in place: delete and recreate instead.

Error codes

Errors a request authenticated with an API key can receive on /api/v2:

Code
HTTP
What the platform observed
What to do
DALP-14386 AUTHENTICATION_REQUIRED
401
No X-Api-Key header, or a key that is unknown, deleted, turned off, expired, without an organisation, or held by a user no longer in its organisation.
Do not retry with the same key. Read the key's record with a session, then enable it or issue a new key.
DALP-0036 SESSION_POLICY_DENIED
403
A read key called a route declared as a write, or a key called a route that accepts only a session.
Do not retry. Use a read-write key, or call the route with a Console session.
DALP-12136 auth.apikey.THROTTLED
429
The key filled its own request window.
Stop sending with the key, then retry after lastRequest plus rateLimitTimeWindow. Retrying at once fails again.
DALP-17440 WALLET_VERIFICATION_HELD
403
The key's holder is in the cooling-off period after a wallet security change, and the request needs wallet verification. Nothing was stored.
Wait until data.heldUntil, or the number of seconds in data.retryAfterSeconds, then send the request again.
DALP-0009 SESSION_RESOLUTION_FAILED
503
The key store did not answer, so the platform could not resolve the caller.
Retry with backoff. The key itself is not at fault.
DALP-14124 AUTH_STORE_ROW_INVALID
500
A stored user, membership or key row behind the key does not have the expected shape.
Do not retry. Contact support with the X-Request-Id response header.

Errors the key management routes under /api/auth return:

Code
HTTP
When it happens
What to do
API_KEY_NOT_SUPPORTED_ON_ROUTE
403
The key called an /api/auth route outside the nine that accept a key.
Call that route with a signed-in Console session.
API_KEY_DELETES_ONLY_ITSELF
403
A key asked to delete a key other than itself.
Delete the other key from a signed-in session of its holder.
API_KEY_NEEDS_CONSOLE_SIGN_IN
403
The Console signs in only through an identity provider, and a create was sent with an Investor portal session.
Sign in to the Console and create the key there. Nothing was stored.
API_KEY_METADATA_INVALID
400
The metadata is missing from a create, is not an object, lacks a valid organisation id, names an organisation you are not a member of, or has an unknown scope.
Send metadata that names an organisation you are a member of, with a scope of read or read-write or no scope. Nothing was stored.
NAME_REQUIRED
400
A create sent no name, or a name of only spaces.
Send a name that is not blank. Nothing was stored.
INVALID_NAME_LENGTH
400
A name is longer than 32 characters, or an update sent a name of only spaces.
Send a name of 1 to 32 characters. No key was created or changed.
EXPIRES_IN_IS_TOO_LARGE
400
expiresIn is longer than 365 days.
Send a shorter expiry, or omit it for a key that never expires.
SERVER_ONLY_PROPERTY
400
The call set a rate-limit, request-budget or permission field.
Remove the field.
WALLET_STEP_UP_REQUIRED
400
A create or power-raising update carried no walletVerification.
Send the wallet proof.
WALLET_STEP_UP_STRONGER_FACTOR_REQUIRED
400
The account holds an authenticator, and the proof was a PIN or recovery code.
Send the authenticator code named in required.
WALLET_VERIFICATION_INVALID_CODE
400
The wallet code was wrong. remainingAttempts says how many tries are left before the lock.
Send the correct code. Nothing was stored.
WALLET_CREDENTIAL_LOCKED
429
Too many wrong wallet codes in a row.
Wait the seconds in retryAfterSeconds, then send a correct code.
WALLET_STEP_UP_UNAVAILABLE
401
The account has no wallet factor.
Set up a PIN on the Console's Security page, then retry.
WALLET_STEP_UP_HELD
403
The holder is in the cooling-off period after a wallet security change.
Wait until heldUntil, then retry.
LOGIN_TWO_FACTOR_REQUIRED
403
enforce2fa is on and the account has not set up two-factor sign-in, unless it signs in only through an identity provider. Applies to a create and a power-raising update.
Set up two-factor on the Console's Security page, then retry.

These are the auth layer's own codes. They come from the routes under /api/auth and carry no DALP-#### id, so match on the code string. Platform API routes answer with the catalogued DALP- codes, whose full entries and recovery copy are on the DALP error index.

Next steps

  • Pagination covers listing resources once you can authenticate.
  • Errors covers the full error envelope and how to branch your integration on it.
  • Install and set up the command-line tool covers dalp login, which creates a read-write key through a browser device-code flow instead of a direct API call.

Was this page helpful?