Skip to content

guides

Intended Documentation

Verify a Token

Verify an Authority Token before any downstream system acts on it — check the Ed25519 signature, the kid-pinned key, tenant and adapter binding, expiry, and single-use nonce.

Verify a Token#

An Authority Token is the cryptographic proof that an action was authorized. Before any downstream system executes the action, it must verify the token. A token that fails verification means do not act — the platform is fail-closed end to end.

The token is an Ed25519 JWT, signed by a per-tenant Ed25519 key, with a 300-second TTL and a single-use nonce. Verification checks the signature against the right key (pinned by kid), confirms the claims bind to the expected tenant and adapter, and enforces the time window. You can verify three ways: through the hosted POST /verify/token endpoint, with the @intended/verify library locally, or with the CLI.

Token lifecycle
A token is minted on approval with a kid, tenant, adapter, decision, and nonce; the verifier selects the matching public key by kid, checks the Ed25519 signature, asserts the bindings and time window, and consumes the nonce so it cannot be replayed.

Mint on approval, verify against the kid-matched key, then consume the nonce. A replayed token fails the single-use check.

What gets checked#

verifyToken (the same logic behind the hosted endpoint and the CLI) enforces all of the following. Any failure returns valid: false with a specific reason:

CheckFailure reason
kid in the token header matches the expected keyKID_MISMATCH
Ed25519 signature is valid for the public keySIGNATURE_VERIFICATION_FAILED
Claims parse into the Authority Token shapeTOKEN_CLAIMS_INVALID
tenantId claim matches your expected tenantTOKEN_TENANT_MISMATCH
adapterId claim matches your expected adapterTOKEN_ADAPTER_MISMATCH
Token is within its time window (60s clock skew allowed)TOKEN_EXPIRED / TOKEN_USED_BEFORE_VALID
TTL does not exceed your configured maximumTOKEN_TTL_EXCEEDS_LIMIT

Pin the kid

Always pass the expected kid. Pinning the key id prevents an attacker from presenting a token signed by an unrelated or rotated key. The verifier rejects a mismatch before it even checks the signature.

Steps#

Fetch the tenant's public keys

Authority keys are per-tenant Ed25519 with statuses ACTIVE / PREVIOUS / RETIRED. Fetch the current set and select the one whose kid matches the token header.

GET/tenants/:tenantId/authority-keys/publicRequires auth

Returns the tenant's verification keys (ACTIVE and PREVIOUS), each with its kid and publicKeyPem.

bash
curl "https://api.intended.so/tenants/$INTENDED_TENANT_ID/authority-keys/public" \
  -H "Authorization: Bearer $INTENDED_API_KEY"

The response is { "tenantId": "…", "keys": [ { "kid": "…", "publicKeyPem": "…", "status": "ACTIVE" }, … ] }. Decode the token header to read its kid, then pick the matching key. To inspect a token's header and claims without verifying:

bash
intended inspect-token --token "$TOKEN"

Verify the token

Verify locally with the library, with the CLI, or through the hosted endpoint. All three run the same checks.

bash
curl -X POST https://api.intended.so/verify/token \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: $INTENDED_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJFZERTQSIsImtpZCI6Im…",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----",
    "expectedKid": "tenant_acme_prod-1717800000-a1b2c3",
    "expectedTenantId": "tenant_acme_prod"
  }'

From the command line, verify returns exit code 0 when valid and exit code 2 when invalid, so it slots into CI gates:

bash
intended verify \
  --token "$TOKEN" \
  --key ./tenant-public.pem \
  --kid tenant_acme_prod-1717800000-a1b2c3 \
  --tenant tenant_acme_prod \
  --adapter github
# exit 0 → valid, exit 2 → invalid

Read the result

The hosted endpoint returns the verification verdict plus the parsed claims and header:

json
{
  "protocolVersion": "intended.authority.v1",
  "verificationModel": "deterministic-asymmetric-hash-chain",
  "valid": true,
  "reason": null,
  "claims": {
    "intentId": "8aa3f5f6-b1a9-4c5b-a29f-b489f7d0be58",
    "tenantId": "tenant_acme_prod",
    "adapterId": "github",
    "decision": "APPROVED",
    "nonce": "f3a9…",
    "issuedAt": "2026-06-07T12:00:00.000Z",
    "expiresAt": "2026-06-07T12:05:00.000Z"
  },
  "header": { "alg": "EdDSA", "kid": "tenant_acme_prod-1717800000-a1b2c3" }
}

When valid is false, reason carries one of the codes from the table above and claims may be null.

Enforce the outcome and consume the nonce

  • valid: true and the claims match what you expected — proceed with the action.
  • valid: false — stop. Do not execute. Re-evaluate through POST /intent if the action still needs to happen.

A token is single-use. The runtime enforces this with a database-unique constraint on (tenantId, nonce): the first execution consumes the nonce, and a replay of the same token is rejected. If you build a custom adapter, consume the nonce as part of acting on the token — the connector SDK's BaseAdapter does this for you (decode kid → fetch the public key → Ed25519-verify with kid pinning → assert tenant/adapter/target → assert decision === "APPROVED" → check expiry → consume the nonce, all before it runs the action).

Errors from the hosted endpoint#

Beyond the verification reason codes above, the /verify/token route returns these before it even runs the cryptographic checks:

StatusCodeCause
400INVALID_VERIFY_TOKEN_REQUESTBody failed schema validation
400VERIFY_TENANT_REQUIREDNo tenant context (send x-tenant-id or expectedTenantId)
403TENANT_MISMATCHexpectedTenantId does not match your credential's tenant
403VERIFY_KEY_NOT_FOUNDThe expectedKid is not registered for this tenant
403VERIFY_KEY_NOT_ACTIVEThe key exists but is RETIRED, not ACTIVE/PREVIOUS
403VERIFY_PUBLIC_KEY_MISMATCHThe supplied PEM does not match the registered key for that kid

JWKS distribution is Roadmap

/.well-known/jwks.json currently serves the physical-AI token signer (an ephemeral local-dev keypair), not per-tenant authority keys. To verify a per-tenant Authority Token today, fetch the key from /tenants/:tenantId/authority-keys/public (or pass the PEM directly to verifyToken). A JWKS HTTP route for per-tenant authority keys is Roadmap.

Next steps#

Verify a Token | Intended