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.
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:
| Check | Failure reason |
|---|---|
kid in the token header matches the expected key | KID_MISMATCH |
| Ed25519 signature is valid for the public key | SIGNATURE_VERIFICATION_FAILED |
| Claims parse into the Authority Token shape | TOKEN_CLAIMS_INVALID |
tenantId claim matches your expected tenant | TOKEN_TENANT_MISMATCH |
adapterId claim matches your expected adapter | TOKEN_ADAPTER_MISMATCH |
| Token is within its time window (60s clock skew allowed) | TOKEN_EXPIRED / TOKEN_USED_BEFORE_VALID |
| TTL does not exceed your configured maximum | TOKEN_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.
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:
Verify the token
Verify locally with the library, with the CLI, or through the hosted endpoint. All three run the same checks.
From the command line, verify returns exit code 0 when valid and exit code 2 when invalid, so it slots into CI gates:
Read the result
The hosted endpoint returns the verification verdict plus the parsed claims and header:
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: trueand the claims match what you expected — proceed with the action.valid: false— stop. Do not execute. Re-evaluate throughPOST /intentif 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:
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_VERIFY_TOKEN_REQUEST | Body failed schema validation |
| 400 | VERIFY_TENANT_REQUIRED | No tenant context (send x-tenant-id or expectedTenantId) |
| 403 | TENANT_MISMATCH | expectedTenantId does not match your credential's tenant |
| 403 | VERIFY_KEY_NOT_FOUND | The expectedKid is not registered for this tenant |
| 403 | VERIFY_KEY_NOT_ACTIVE | The key exists but is RETIRED, not ACTIVE/PREVIOUS |
| 403 | VERIFY_PUBLIC_KEY_MISMATCH | The 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#
- Decision Token Model — the full token structure and trust model
- Quickstart — mint a token by submitting an intent
- API Authentication — credentials, headers, and errors