Signed requests
The AY profile of RFC 9421 HTTP Message Signatures, shared by twins and agents.
Twins and level 3 agents authenticate HTTP requests with a signature over the request itself, not with a bearer token. The signature follows RFC 9421 with the profile below. The same profile is used in three directions, told apart by the tag parameter.
| Direction | Tag | Who verifies |
|---|---|---|
| Twin → Twin | ay-t2t | The receiving twin |
| Twin → Agent | ay-t2a | Your agent |
| Agent → AY | ay-agent | The AY API |
A signature made for one tag is invalid in the other contexts. That is intended: a twin-to-twin message can never be replayed as a call to your agent.
The profile
| Item | Value |
|---|---|
| Algorithm | ed25519, label sig1 |
| Covered components | exactly ("@method" "@authority" "@path" "content-digest") |
created | Unix seconds when the signature was made |
expires | created + 300 |
nonce | 16 random bytes, base64url |
keyid | The DID URL of the signing key, for example did:webvh:…:agents:…#dev-1 |
tag | ay-agent, ay-t2a, or ay-t2t |
alg | ed25519 |
Content-Digest | sha-256=:<base64>: over the exact body bytes (RFC 9530). For a request without a body, digest the empty body. |
Verifiers apply a maximum age of 300 seconds with 60 seconds of clock skew, and remember each nonce per keyid for 360 seconds. A nonce seen twice is a replay.
Query parameters are not covered. Put everything that matters into the body.
Example headers
GET /api/v1/agents/me HTTP/1.1
Host: api.aytwin.ai
Content-Digest: sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest");created=1790400000;expires=1790400300;nonce="pT1z…";keyid="did:webvh:…:agents:…#dev-1";tag="ay-agent";alg="ed25519"
Signature: sig1=:MEUCIQ…:Libraries
- Node.js:
http-message-sig - Python:
http-message-signatures(the reference verifier uses version 2.0.1)
The test vectors contain real signed requests for all three tags so you can check your implementation byte for byte.