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/public

Returns the tenant's verification keys (ACTIVE and PREVIOUS), each with its kid and publicKeyPem. Public keys only, so no credential is needed. Any relying party can fetch them.

bash
curl "https://api.intended.so/tenants/$INTENDED_TENANT_ID/authority-keys/public"

The same keys are served as a standard JWKS at /.well-known/authority/:tenantId/jwks.json (also public, cacheable for 5 minutes).

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
# 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": "decision-only",
    "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: each carries a unique nonce. Intended never runs the action, so the service that acts on the token enforces single use: record each accepted (tenantId, nonce) pair (a unique constraint works well) and reject a token whose nonce you have already seen. Do this as part of acting, in this order: decode kid → select the public key → Ed25519-verify with kid pinning → assert tenant and target → assert decision === "APPROVED" → check expiry → consume the nonce → act.

If you verify through the hosted endpoint, Intended can record the use for you: send "consume": true (with expectedKid). The first such call for a valid token answers valid: true, consumed: true; any later one answers valid: false, consumed: false with reason TOKEN_ALREADY_CONSUMED, including concurrent calls. Without consume the hosted check is read-only and does not use the token up. Single use holds only where the acting service enforces it: with consume: true here, or with its own nonce record. A signature check alone accepts the same token for its whole lifetime. Note that an idempotent retry of POST /intent (same Idempotency-Key, within 24 hours) returns the original token again, which may already be consumed.

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_CONSUME_REQUIRES_KIDconsume: true was sent without expectedKid
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

Where to get the public key

/.well-known/jwks.json serves the physical-AI token signer, not per-tenant authority keys. To verify a per-tenant Authority Token, fetch the key from /tenants/:tenantId/authority-keys/public or the JWKS at /.well-known/authority/:tenantId/jwks.json (both public), or pass the PEM directly to verifyToken.

Next steps#

Verify a Token | Intended