guides
Intended Documentation
Verify Decision Tokens
Verify Ed25519 Authority Tokens locally with @intended/verify — pin the kid, validate claims, enforce the 300s TTL, and reject on the exact failure reasons the verifier returns.
Verify Authority Tokens#
The authorityDecisionToken returned by POST /intent is an Ed25519 JWT bound to exactly one intent, tenant, and target. Intended decides and signs; it never runs the action. Before your agent's action takes effect, the service that receives it (or the gateway you run in front of it) verifies the token locally — it never needs to call back to the Intended runtime. Verification is fail-closed: any failed check rejects the action.
This page uses @intended/verify's verifyToken, which is built on jose. Single-use enforcement (nonce consumption) is the verifying service's job; the last step below covers it.
Minted on APPROVED, verified locally, consumed once. Kid mismatch, bad signature, claim mismatch, and expiry are all rejected.
Token claims#
The validated claim set (from AuthorityDecisionTokenClaimsSchema):
| Claim | Meaning |
|---|---|
intentId | The intent this token authorizes (UUID). |
tenantId | Tenant scope. |
adapterId | The consumer the token is bound to. Decisions from /v1/agent-intent carry decision-only. |
adapterTarget | The specific target the action is bound to. |
targetSystem | The system the action targets. |
proposedAction | The action that was approved. |
decision | Always APPROVED — only approvals mint tokens. |
issuedAt / expiresAt | ISO-8601 validity window. |
nonce | ≥32-character value enforcing single use. |
At signing time the runtime also sets the registered JWT claims iss: "intended-authority", aud: <adapterId>, and integer iat / exp. The header is { "alg": "EdDSA", "typ": "JWT", "kid": "<tenant-key-id>" } — kid is mandatory.
Local verification flow#
Decode the header and read kid
Read the kid from the JWT header and load the tenant's public key for that kid. Pinning kid is mandatory; an unpinned or unknown kid is rejected with KID_MISMATCH.
Verify the Ed25519 signature
Verify against the public key, restricted to the EdDSA algorithm, with the expected issuer and audience. A failed signature check returns SIGNATURE_VERIFICATION_FAILED.
Validate the claims
The verifier parses the claims against the schema, then asserts tenantId and adapterId match what you expect, the issuedAt/expiresAt window is well-formed, the token is not used before valid or past expiry (within clockSkewSeconds), and the TTL does not exceed maxTokenTtlSeconds.
Consume the nonce
verifyToken itself does not touch a datastore — it is pure verification. Single-use enforcement is the verifying service's responsibility: record each accepted (tenantId, nonce) atomically (a unique constraint works well) and reject a token whose nonce has already been consumed, before you act.
With the TypeScript SDK#
From @intended-inc/sdk 0.8.0, guard() does all of this for you before your callback runs: it fetches the tenant's public key for the token's kid, calls POST /verify/token with consume: true, and runs the callback only if that call used the token up. A token that is invalid, expired or already used returns status: "denied" and the callback does not run. Pass { verifyToken: "verify" } to check without consuming (when a downstream service consumes the token), or "off" to skip the check, which logs a warning because the decision is then advisory only. This is a breaking change in 0.8.0: if your callback passes the token to a service that consumes it, use { verifyToken: "verify" }.
Checking a token needs intent:create or token:read (the "Write" preset has both; "Read" has token:read), verify:token or audit:read. Consuming it (consume: true) needs intent:create (the "Write" preset); any other key, including one with verify:token, gets 403 MISSING_CONSUME_SCOPE. Re-consuming a token with the same API key that submitted it answers TOKEN_ALREADY_CONSUMED but is not treated as a token replay by Intended's abuse detection.
/verify/* is rate limited to 600 requests per API key per 5 minutes by default (429 VERIFY_RATE_LIMITED with Retry-After).
The console's Insights page shows the share of allowed actions in the last 7 days whose token was verified with consume: true. Tokens checked only offline are not counted there.
Each credential gets a share of the tenant's /verify/* rate window, so one busy or misbehaving verifier is refused on its own (VERIFY_RATE_LIMITED) without blocking your other verifiers. VERIFY_TENANT_RATE_LIMITED means several credentials together filled the tenant window; Intended is alerted when that happens.
Verifying in code#
verifyToken returns { valid, reason, claims, header }. On success reason is null and claims is the validated claim set; on failure valid is false and reason names the failure.
Failure reasons#
These are the exact reason values verifyToken returns. Treat every one as a hard reject.
| Reason | Cause |
|---|---|
KID_MISMATCH | Token kid does not match the pinned expectedKid. |
SIGNATURE_VERIFICATION_FAILED | Signature, issuer, or audience check failed. |
TOKEN_CLAIMS_INVALID | Claims fail schema validation. |
TOKEN_TENANT_MISMATCH | Token tenantId ≠ expectedTenantId. |
TOKEN_ADAPTER_MISMATCH | Token adapterId ≠ expectedAdapterId. |
TOKEN_CLAIMS_INVALID_TIME | issuedAt / expiresAt not parseable. |
TOKEN_TIME_WINDOW_INVALID | expiresAt ≤ issuedAt. |
TOKEN_USED_BEFORE_VALID | Now + skew is before issuedAt. |
TOKEN_EXPIRED | Now − skew is past expiresAt. |
TOKEN_TTL_EXCEEDS_LIMIT | TTL exceeds maxTokenTtlSeconds (the 300s cap). |
Warning
verifyToken is signature + claim verification only. It does not enforce single use — that requires consuming the nonce. Track and reject seen nonces in the service that acts on the token.
On-robot verification (Physical AI)#
For physical-AI runtimes, the Authority Token minted by POST /v1/physical/authority-tokens is verified at the robot or controller before actuation lands, so a compromised network path or stale cloud session cannot let an actuation through. The physical-AI signer signs with RS256, not the Ed25519 used by the general /intent token above, and publishes its public key at /.well-known/jwks.json. In production, when INTENDED_PHYSICAL_KMS_KEY_ID is set, the signer is backed by a persistent AWS KMS key; in dev/sandbox it falls back to an ephemeral per-process keypair that rotates on restart, so cache JWKS short and refresh on a kid miss.
Any standard JWT library can verify a physical token. Check:
| Check | Expected value |
|---|---|
| Algorithm | RS256 only; resolve the key by the header kid from the JWKS |
iss | https://api.intended.so |
aud | intended-edge-verifier |
exp | In the future (allow a small clock-skew leeway) |
intended.tenantId | The tenant this robot is provisioned to |
sub / intended.actorIdentity | This robot's registered actorIdentity |
jti | Not seen before, if your controller enforces single use |
Next steps#
- Authority Token Model — full structure, keys, and lifetime.
- Verify a Token (Guide) — the hosted endpoint, library and CLI side by side.
- Enforcement SDK — the SDK method reference.
- Error Patterns — runtime error codes. </content>