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.

CredentialFormatUsed by
API keycmp_live_<12 hex>.<43 chars>Your own software, against Live
Sandbox API keycmp_test_<12 hex>.<43 chars>Your own software, against Sandbox
OAuth access tokencmp_oat_<12 hex>.<43 chars>A connected AI assistant
Authenticating
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:

@veriticity/sdk
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.

RFC 8414 discovery
# Start here rather than hard-coding endpoints.
curl https://app.veriticity.com/.well-known/oauth-authorization-server
Response
{
  "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.

  1. Send the person to /oauth/authorize with your client_id, redirect_uri, code_challenge, state and the scopes you need.
  2. 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.
  3. They return to your redirect_uri with a code and an iss. Check iss and state.
  4. Exchange the code within 60 seconds.
Token exchange
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
200 OK
{
  "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#

CredentialLifetime
Authorization code60 seconds, single use
Access token1 hour
Refresh token90 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#

OperationAgent-bound keyOrganisation keyOAuth connection
Submit a purchaseYesNopurchase:request
Read back or list its own purchasesYesNopurchase:read
Cancel a request still waiting for approvalIts ownAny in the organisationpurchase:request, its own
Read its agent, spending rules and budget statusYesNopurchase:check
Simulate against current policiesYes, as itselfYes, must name agentIdpurchase:check
Get an execution token for its own authorizationYes, the key that submitted itNoNo scope reaches this
Use a purchase authorizationIts own agent’s, with the execution tokenYesNo scope reaches this
Read, list or void authorizationsNoYesNo scope reaches this
Withdraw an unused authorizationOf a purchase it submittedYespurchase:request, of a purchase it submitted
Read policies, list agentsNoYesNo scope reaches this
Simulate a named policy versionNoYesNo scope reaches this
Manage webhooksNoYesNo scope reaches this
Write policyNoNoNo
MCP toolsAPI keys refusedAPI keys refusedPer-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.

ScopeWhat the person is consenting to
purchase:checkCheck whether a purchase would be allowed
purchase:requestRequest a purchase
purchase:readSee 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:

CodeWhat to do
insufficient_scopeRe-authorise with a wider grant. This will work.
oauth_not_permittedStop. No scope reaches this operation, so reconnecting changes nothing.
connection_unauthorizedStop 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.