Authentication
How to authenticate against the Veriticity API. Agent-bound and organisation-scoped API keys, OAuth connections for AI assistants, scopes, and which credential reaches which endpoint.
Two credentials, one header#
Two things authenticate against the Veriticity API, and both are presented the same way.
| Credential | Format | Used by |
|---|---|---|
| API key | cmp_live_<12 hex>.<43 chars> | Your own software, against Live |
| Sandbox API key | cmp_test_<12 hex>.<43 chars> | Your own software, against Sandbox |
| OAuth access token | cmp_oat_<12 hex>.<43 chars> | A connected AI assistant |
curl https://app.veriticity.com/v1/agents \
-H "Authorization: Bearer cmp_live_9f2a1c4b7e30.<secret>"There is no X-API-Key header and no query-parameter form. A token in a URL ends up in access logs, browser history and Referer headers, so the header is the only way in.
The TypeScript SDK takes either credential and builds the same header:
import { Veriticity } from "@veriticity/sdk";
// An API key -- agent-bound or organisation-scoped. Both are cmp_live_, and the
// difference between them is an authority Veriticity holds, not a format.
const veriticity = new Veriticity({ apiKey: process.env.VERITICITY_API_KEY! });
// An OAuth access token, from a connection somebody authorised.
const asConnection = new Veriticity({ accessToken: "cmp_oat_..." });
// Or a supplier, called before every request. Access tokens last 60 minutes.
const longLived = new Veriticity({
accessToken: () => tokenStore.currentAccessToken(),
});Pass a function for accessToken whenever the client outlives a token. The SDK calls it before every request, and never caches, refreshes or stores what it returns — for the reasons under OAuth connections below.
An agent never authenticates#
This is the single most important sentence on this page, and getting it wrong makes everything else confusing.
- An agent is an identity — a named thing that is allowed to spend within rules. It holds no credential and presents nothing.
- An API key may be bound to an agent. The key authenticates; it then acts as that agent.
- An OAuth connection is one person’s grant letting one application act as one existing agent. The token authenticates; the connection acts as the agent.
So the correct phrasings are “an API key bound to an agent” and “an OAuth connection acting as an agent”. There is no sense in which an agent logs in.
API keys#
Keys bound to an agent are issued in the dashboard by an owner, and the secret is shown once. Only a hash is stored, so a lost key is replaced rather than recovered.
There are two kinds, and the difference is an authority:
Bound to an agent#
The credential your agent uses. It submits purchases and reads back its own, and it simulates as itself. It cannot name a different agent, cannot read the policies it is judged against, and cannot change them. An agent may hold several keys, rotated and revoked independently.
Rotating a key in the dashboard issues a replacement and gives the key it replaces an end date — 24 hours later by default. Both authenticate until then, so a deployment can move to the new one without an interruption. After that the old key is refused with api_key_expired, and its last day is visible on the key itself before it arrives.
Execution tokens#
To use an approved purchase’s authorization, the key that submitted the purchase asks for an execution token and sends it in the Veriticity-Execution-Token header, beside its own key. A token begins cmp_pex_, is shown once, and works for that one authorization until it is used, withdrawn, voided or expires. It is not a credential on its own: it is refused anywhere else, and refused without the agent’s key. See Purchase authorization.
Organisation-scoped#
Acts for the organisation rather than for any one identity. It reads the organisation’s agents and policies, simulates against named policy versions, manages webhooks, and reads, uses, withdraws and voids purchase authorizations. It cannot submit purchases, because it names no identity to attribute one to. The dashboard does not issue organisation-scoped keys. Your organisation’s members manage webhooks and withdraw or void authorizations in the dashboard instead.
OAuth connections#
OAuth is how an external AI assistant — Claude, ChatGPT, or your own — comes to act as an agent that already exists. A person authorises it, chooses which agent it acts as, and sees what it will be able to do before they agree. Owners, admins and users can connect an assistant. Viewers can’t.
The assistant does not become a Veriticity user, does not create an agent, and gains no authority of its own.
Start with discovery#
Endpoints are published, so nothing needs hard-coding into a configuration file that somebody has to keep correct.
# Start here rather than hard-coding endpoints.
curl https://app.veriticity.com/.well-known/oauth-authorization-server{
"issuer": "https://app.veriticity.com",
"authorization_endpoint": "https://app.veriticity.com/oauth/authorize",
"token_endpoint": "https://app.veriticity.com/oauth/token",
"revocation_endpoint": "https://app.veriticity.com/oauth/revoke",
"scopes_supported": ["purchase:check", "purchase:request", "purchase:read"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"authorization_response_iss_parameter_supported": true,
"client_id_metadata_document_supported": true
}The flow#
Authorization Code with PKCE. S256 only — plain is refused.
- Send the person to
/oauth/authorizewith yourclient_id,redirect_uri,code_challenge,stateand the scopes you need. - They sign in, choose the organisation and the agent, and approve the scopes. Your request cannot name either — a client that could name a tenant would be choosing whose money it was about to spend.
- They return to your
redirect_uriwith acodeand aniss. Checkissandstate. - Exchange the code within 60 seconds.
curl -X POST https://app.veriticity.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=cmp_oac_... \
-d redirect_uri=https://your-app.example.com/callback \
-d code_verifier=<the PKCE verifier> \
-d client_id=https://your-app.example.com/client.json{
"access_token": "cmp_oat_4d1e8a90c375.7mZx...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "cmp_ort_8b2f1c94e05a.Qw3p...",
"scope": "purchase:check purchase:request"
}Token requests are limited to 300 a minute from one client address. Over the limit, /oauth/token answers 429 with Retry-After in seconds and {"error": "temporarily_unavailable"}; nothing was issued or rotated, and the refresh token you sent is still valid. Retry after that many seconds.
Lifetimes and rotation#
| Credential | Lifetime |
|---|---|
| Authorization code | 60 seconds, single use |
| Access token | 1 hour |
| Refresh token | 90 days, sliding — each rotation resets it |
Registering a client#
Veriticity accepts Client ID Metadata Documents from a fixed list of supported AI assistants: Claude (documents on claude.ai) and ChatGPT (on chatgpt.com). Their client_id is an HTTPS URL to a document they host, so they connect without anybody registering anything in advance. A client_id on any other origin is refused, because the consent screen shows the name the document gives, and that name has to be one we’ve checked.
Dynamic Client Registration is not supported. To connect your own OAuth or MCP client, ask us to register it: send its name, what it does and its exact redirect URIs through Veriticity support. A registered client is given a client_id (and, for a server-side client, a secret) for Live; Sandbox is registered separately.
Which credential reaches what#
| Operation | Agent-bound key | Organisation key | OAuth connection |
|---|---|---|---|
| Submit a purchase | Yes | No | purchase:request |
| Read back or list its own purchases | Yes | No | purchase:read |
| Cancel a request still waiting for approval | Its own | Any in the organisation | purchase:request, its own |
| Read its agent, spending rules and budget status | Yes | No | purchase:check |
| Simulate against current policies | Yes, as itself | Yes, must name agentId | purchase:check |
| Get an execution token for its own authorization | Yes, the key that submitted it | No | No scope reaches this |
| Use a purchase authorization | Its own agent’s, with the execution token | Yes | No scope reaches this |
| Read, list or void authorizations | No | Yes | No scope reaches this |
| Withdraw an unused authorization | Of a purchase it submitted | Yes | purchase:request, of a purchase it submitted |
| Read policies, list agents | No | Yes | No scope reaches this |
| Simulate a named policy version | No | Yes | No scope reaches this |
| Manage webhooks | No | Yes | No scope reaches this |
| Write policy | No | No | No |
| MCP tools | API keys refused | API keys refused | Per-tool scope |
Response shapes do not vary by credential. The same endpoint returns the same body whether an API key or a connection called it.
Scopes#
There are exactly three, and no scope implies another. Holding purchase:request does not grant purchase:check, and the reverse is equally false.
| Scope | What the person is consenting to |
|---|---|
purchase:check | Check whether a purchase would be allowed |
purchase:request | Request a purchase |
purchase:read | See what happened to purchases it requested |
There is no full_access scope, no implicit scope, and no client_credentials grant. A client-credentials token would carry an application and an organisation but no person and no agent, which is an organisation-scoped API key with extra ceremony.
When a credential stops working#
Revocation takes effect on the next call, not at the end of a token’s hour. The connection, the client, the organisation and the granting person’s membership and role are all re-read on every request.
That means a connection can stop working without your credential changing at all. Three refusals matter, and they are not interchangeable:
| Code | What to do |
|---|---|
insufficient_scope | Re-authorise with a wider grant. This will work. |
oauth_not_permitted | Stop. No scope reaches this operation, so reconnecting changes nothing. |
connection_unauthorized | Stop and tell the person. The human behind the grant has left or lost permission; refreshing will succeed and the next call will fail identically. |
The full list is on Errors.

