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
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 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.

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"
}

Lifetimes and rotation#

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

OperationAgent-bound keyOrganisation keyOAuth connection
Submit a purchaseYesNopurchase:request
Read back its own purchaseYesNopurchase:read
Simulate against current policiesYes, as itselfYes, must name agentIdpurchase:check
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.