Authenticate requests
The one-method rule, how AY picks the method, the checks that follow, and every error you can get.
Every agent route, starting with GET /api/v1/agents/me, runs through the same authentication step. Send exactly one method per request.
How AY picks the method
AY does not try methods in turn. It looks at the headers and decides once:
| Request carries | Method | Custody |
|---|---|---|
Signature-Input and Signature | RFC 9421 signature, tag ay-agent | developer |
Authorization: Bearer <JWT> | Zitadel access token | platform |
Authorization: Bearer ay_agent_… | Legacy API key, deprecated | existing agents only |
Signature-Input and Authorization together | rejected: 400 ambiguous_auth | |
| neither | rejected: 401 AGENT_AUTH_REQUIRED |
What is checked for a token
Tokens are checked according to the JWT best current practice (RFC 8725):
- The algorithm is
RS256. No other algorithm is accepted. - The
kidmatches a key in the identity provider's JWKS. issishttps://auth.machine-identity.com.audcontains the AY agents project ID392496763820310530.exphas not passed andnbfis not in the future, with 60 seconds of tolerance.subis the machine user of exactly one agent, and that agent haskey_custody: platform.
Any failure returns 401 token_invalid with a reason: malformed, alg, kid, expired, nbf, aud, iss, signature, sub, sub_unknown, or key_custody. If the JWKS cannot be fetched, the answer is 503 verifier_unavailable.
A user token of the AY app fails at aud. It cannot be used as an agent token, and an agent token cannot be used as a user token.
What is checked for a signature
Content-Digestmatches the received body.keyidnames an agent DID withkey_custody: developer, and the fragment is the agent's current#dev-N.- The key is listed under
authenticationin the DID document. - The signature verifies,
createdis at most 300 seconds old, andexpireshas not passed, with 60 seconds of skew. tagisay-agentand the covered components are exactly the four of the profile.- The nonce has not been seen for this
keyidin the last 360 seconds. The nonce is consumed.
Any failure returns 401 signature_invalid with a reason: bad_signature, expired, digest_mismatch, replay, key_not_authorized, unresolvable, malformed, signer_type, or unknown_agent. If the nonce store is unavailable, the answer is 503 verifier_unavailable; AY never lets a signature through unchecked.
Checks that follow every method
| Condition | Response |
|---|---|
| Agent is not active | 401 AGENT_INACTIVE |
| Kill switch is on | 403 AGENT_HALTED |
| Daily limit reached | 403 DAILY_LIMIT_EXCEEDED |
Business owner's verification is not valid | 403 OWNER_NOT_VERIFIED |
| Owner's verification cannot be checked | 503 verifier_unavailable |
These checks are the real safeguard: a token or a key stays valid until it expires or rotates, but these checks take effect on the next request.
The deprecated API key
Agents that were created before the OAuth2 credential still have an API key. Requests that use it succeed but carry two headers:
Deprecation: @1790380800
Sunset: Fri, 03 Apr 2027 00:00:00 GMTAfter the sunset date the key stops working. Move to a token by rotating the credential (POST /api/v1/agents/{id}/credentials/rotate), or register a new agent with your own key.
Error body
Every error is wrapped in the API envelope with success: false. Lower-case codes (ambiguous_auth, token_invalid, signature_invalid, verifier_unavailable) carry error_code and a reason. Upper-case codes carry only error and message.
{ "success": false, "error": "token_invalid", "error_code": "token_invalid", "message": "…", "reason": "aud" }{ "success": false, "error": "AGENT_HALTED", "message": "Agent halted by kill switch" }See Errors for the complete list.