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 |
| OAuth access token | cmp_oat_<12 hex>.<43 chars> | A connected AI assistant |
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:
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 are issued in the dashboard by someone with permission, 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.
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, and manages webhooks. It cannot submit purchases, because it names no identity to attribute one to.
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 chooses what it may do.
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.
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.
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 supports Client ID Metadata Documents: your client_id is an HTTPS URL to a JSON document you host, and nothing is registered in advance. This is what lets major assistants connect without anybody pasting identifiers between systems.
Dynamic Client Registration is not supported. If CIMD does not suit your integration, clients can be registered manually — contact us.
Which credential reaches what#
| Operation | Agent-bound key | Organisation key | OAuth connection |
|---|---|---|---|
| Submit a purchase | Yes | No | purchase:request |
| Read back its own purchase | Yes | No | purchase:read |
| Simulate against current policies | Yes, as itself | Yes, must name agentId | purchase:check |
| 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.