Content-Digest. Agents that don’t support signing may continue using their existing authentication path.
This is the practical implementation guide. For the normative specification — covered components, canonicalization rules, the full verifier checklist, replay dedup sizing, and the complete error taxonomy — see Security: Signed Requests.
Code examples below use JavaScript/TypeScript, Python, and Go SDK helpers in tabs. All three SDKs implement the same RFC 9421 profile against the same conformance vectors — the API surface differs but the wire output is identical. If your language isn’t listed, the conformance vectors and the normative spec are language-agnostic.
When you need this
Key concepts
Signature coverage
The AdCP signing profile covers these request components:@method— HTTP method@target-uri— full canonicalized request URL@authority— lowercased host headercontent-type— media typecontent-digest— SHA-256 or SHA-512 hash of the exact request body bytes; REQUIRED for every body-bearing request signed under the 3.2 profile
@target-uri and
@authority. Thus https://example.com./p signs as
https://example.com/p with authority example.com; U-label and A-label IDN
forms produce the same bytes. Multiple trailing dots are malformed and MUST be
rejected. This codifies the shipping SDK behavior under
DR-0008.
Key separation
Every agent publishes signing keys with a distinctkid and adcp_use tag:
adcp_use: "request-signing"— for signing outbound tool calls and outbound webhooksadcp_use: "webhook-signing"— deprecated; still accepted on the webhook path for backward compatibility
request-signing key under a separate kid for webhook delivery rather than reusing the same key material for tool calls and webhooks.
Discovery chain
Verifiers find your public key starting from your agent URL:/.well-known/brand.json. Many brand.json files can list the same agent URL, so only the one your agent points to counts. The domain-derived /.well-known/brand.json lookup is a 3.x fallback for webhooks from older sellers, never for request signatures. The full algorithm and its error codes are in Discovering an agent’s signing keys via brand_json_url.
The TypeScript SDK’s BrandJsonJwksResolver (see JWKS resolver options) runs this chain with caching and refresh. It calls get_adcp_capabilities on the agent URL, including when the seller already holds an onboarding record for the agent.
Step 1: Generate a signing key
CLI
private.jwk— the private key (JWK withdfield). Keep this secret.public-jwks.json— the public key in JWKS format. Publish this.
Programmatic
- JavaScript/TypeScript
- Python
- Go
Supported algorithms
Storing the private key
Pick the strongest option your runtime supports. From most to least secure:- Cloud KMS (GCP Cloud KMS, AWS KMS, Azure Key Vault): the private key is generated inside the HSM and never leaves it. Signing is performed by calling the KMS API; you only ever hold the JWK reference, not the key bytes. The TypeScript SDK exposes
createKmsSignerfor GCP Cloud KMS — see@adcp/client/signing/kms. Recommended for any agent handling spend-committing operations. - Secret manager (GCP Secret Manager, AWS Secrets Manager, HashiCorp Vault): load at boot, keep in memory for the process lifetime. Easier than KMS but the key material lives in your process — leaks via memory dumps, logging, or compromised dependencies.
- Environment variable:
ADCP_SIGNING_PRIVATE_KEY='{"kid":"...","kty":"OKP",...}'. Acceptable for dev and small deployments. Same memory-resident risk as a secret manager. - File: development only. Never commit to version control. Use mode
0600andO_EXCLso an existing file is never overwritten —Path.write_bytesinherits the process umask (often0644, world-readable) and is unsafe for private-key material.
Once you choose KMS, signing latency rises (one round-trip to the HSM per request). Profile under load before committing — the TypeScript and Python SDKs cache JWK metadata aggressively and can sustain hundreds of signs/sec against GCP KMS in our internal tests, but your numbers depend on region and concurrency.
Step 2: Publish your public keys
JWKS endpoint
Serve a JSON Web Key Set at a stable HTTPS URL (defaults to/.well-known/jwks.json):
d field. Set Cache-Control: max-age=3600 or similar. If you use separate key material for webhook delivery, publish a second request-signing JWK with a distinct kid. Deprecated webhook-signing JWKs remain accepted on the webhook path for backward compatibility.
brand.json
Serve at/.well-known/brand.json on your brand domain. The agents[].url must be your exact agent URL, and the jwks_uri is how verifiers find your keys:
Capabilities pointer
Your agent’sget_adcp_capabilities response carries identity.brand_json_url, the HTTPS URL of the brand.json above. The host of that URL must share a registrable domain (eTLD+1) with your agent URL, unless the brand.json delegates your agent’s domain through authorized_operators[].
Buyers that only call sellers still answer
get_adcp_capabilities. A verifier starts from your agent URL, so a buyer that signs requests must answer get_adcp_capabilities there, over MCP or A2A, even if it hosts no other tools. A seller’s onboarding record doesn’t remove this. The record must be re-confirmed against your identity.brand_json_url within the brand.json cache lifetime (see Agent resolution). A buyer that serves nothing at its agent URL fails verification with request_signature_capabilities_unreachable.Verifiers read only identity from this response. The response schema still requires adcp and a non-empty supported_protocols, whose values commit the agent to the matching seller-side storyboards, and there is no buyer-only value yet. A path for signers with no capabilities endpoint, and a response shape for buyers that serve nothing else, are under discussion in #6105.Step 3: Sign outbound requests (buyer / orchestrator)
Wrapping fetch / HTTP client
- JavaScript/TypeScript
- Python
- Go
createSigningFetch wraps any fetch-compatible function to sign outbound requests automatically:Capability-aware signing
buildAgentSigningFetch checks whether the target seller supports signed-requests and only signs when supported. This is the recommended approach for production:
Step 4: Verify inbound signatures (seller)
Framework middleware
- JavaScript/TypeScript
- Python
- Go
For raw Express routes, mount
createExpressVerifier after a raw-body middleware. Use mcpToolNameResolver for the resolveOperation callback — it parses the JSON-RPC envelope and returns the MCP tool name:Composing signature + bearer auth with requireAuthenticatedOrSigned
requireAuthenticatedOrSigned bundles the full composition: presence-gated routing (signature auth when headers present, fallback otherwise) plus requiredFor enforcement — unauthenticated requests for signing-required operations get 401 request_signature_required even when no credentials at all are supplied.
MUTATING_TASKS is the full list of spend-committing and state-changing operations exported from @adcp/client/server — use it rather than maintaining your own list.
JWKS resolver options
Step 5: Verify inbound webhooks (buyer / orchestrator)
When sellers send webhooks, verify the signature to confirm authenticity. The webhook profile uses the same RFC 9421 mechanics as request signing but withtag="adcp/webhook-signing/v1" and Content-Digest always covered (no opt-out).
- JavaScript/TypeScript
- Python
- Go
Step 6: Sign outbound webhooks (seller)
- JavaScript/TypeScript
- Python
- Go
Pass a
signerKey to createAdcpServer and the framework signs every outbound webhook automatically:"adcp_use": "request-signing". The webhook verifier still accepts deprecated "webhook-signing" keys for backward compatibility, but new signers should use request-signing. If you want independent webhook rotation or blast-radius isolation, publish a separate request-signing JWK with a webhook-specific kid.
Step 7: Declare the capability
If your seller verifies inbound signatures, declaresigned_requests (alias request_signing in the on-wire schema) in your get_adcp_capabilities response so buyers know to sign:
For AdCP 3.2, covers_content_digest: 'required' is part of the signing profile rather than an operation-level choice. The operation lists still control where signing is supported, shadowed, or required; they never weaken body binding. Keep either or forbidden only on an explicitly configured 3.0/3.1 compatibility endpoint.
- JavaScript/TypeScript
- Python
- Go
get_adcp_capabilities and read request_signing.required_for and supported_for to know which operations you expect them to sign.
Web Bot Auth profile (3.3, experimental)
The Web Bot Auth profile follows the IETF Web Bot Auth draft as written, so any conforming Web Bot Auth library can sign for AdCP. Its wire rules (signature parameters and algorithms, the key directory, nonce and replay, and failure responses) are on the Web Bot Auth profile page (#7894), which also compares it with the RFC 9421 profile above. Identity, trust records, binding, and authorization are in Web Bot Auth identities. This section lists the setup steps only. Signer setup. These steps let a seller verify which origin is calling:- Give each agent its own origin. An agency hosting several agents on one host moves each to a subdomain.
- Serve the key directory at that origin, as Key directories specifies. Regenerate it before its signatures expire.
- Sign each request and webhook as Profile rules specifies, for sellers that advertise
web-bot-authinrequest_signing.profiles(added by #7894).
- Serve a signed revocation list at the origin,
/.well-known/governance-revocations.json(Transport revocation), even while it revokes nothing. Without it, a seller can verify you but cannot bind you for spend. - List the origin in
https://{registrable domain}/.well-known/trust.jsonwith its role (adcp:buyingfor a buyer). The record carries no keys.
- Verify signatures with the profile’s Verification procedure, and answer failures as its Error responses section says.
- Treat a verified origin you have not bound as identity-established: no account access. Evaluate the operator’s trust record only when you bind a principal, check a grant, or apply an
openonboarding mode. - Once you have bound an origin, refuse RFC 9421 signatures for it.
- Advertise
web-bot-authinrequest_signing.profiles(added by #7894).
Key rotation
The JWKS endpoint supports multiple keys simultaneously for zero-downtime rotation:- Generate a new keypair with a new
kid - Add the new public key to JWKS (both old and new are published)
- Update signing configuration to use the new private key
- After 24–48 hours, remove the old public key from JWKS
kid to revoked_kids in your revocation list and rotate to a new key immediately. See Revocation for the revocation list format.
Testing
Conformance vectors
The spec ships legacy-profile wire vectors, dedicated 3.2 wire vectors, and the 3.2 post-parse body-integrity reference-evaluator matrix at/compliance/latest/test-vectors/request-signing/ (source at static/compliance/source/test-vectors/request-signing/). Select fixtures by signing_profile_version; the root positive and negative directories are 3.1 compatibility fixtures, not 3.2 conformance cases.
- Legacy 3.1 positive vectors: valid signed requests using the legacy binary encoding
- Legacy 3.1 negative vectors: invalid requests a 3.1 verifier must reject with the stated error code
- Profile-3.2 wire vectors: 3.2 versions of the legacy positive vectors and of the checklist step 2–12 negatives. They sign with
content-digestcovered (except the one negative that tests its absence) and assume a verifier advertisingcovers_content_digest: "required". The set also includes rejections of malformed RFC 8941sf-binaryand malformed authorities.
Grade your verifier
Error codes
When verification fails, return401 with WWW-Authenticate: Signature error="<code>".
The negative vectors grade <code> byte-for-byte against the vector’s expected_outcome.error_code, so a rejection that is correct in substance still fails its vector if the code string differs. Take the code from the taxonomy rather than from memory.
For the codes — with retry semantics for each, and the rules for the WWW-Authenticate header itself — see Transport error taxonomy. Signing-key discovery via brand_json_url raises its own request_signature_brand_* and request_signature_key_origin_* codes, listed under Discovering an agent’s signing keys via brand_json_url.
Related
- Security: Signed Requests — normative spec with verifier checklist, canonicalization rules, and replay dedup sizing
- Push Notifications — webhook setup including signature verification
- Validate Your Agent — full compliance validation including signing conformance
- Build an Agent — SDK setup and storyboard validation
- RFC 9421 — HTTP Message Signatures specification