Skip to main content
AdCP supports HTTP Message Signatures (RFC 9421) for cryptographic request authentication. A buyer signs outbound requests so the seller can verify who sent them and that the payload wasn’t tampered with. A seller signs outbound webhooks so the buyer can verify authenticity. Signing remains optional through AdCP 3.2 and becomes mandatory in AdCP 4.0 for all spend-committing operations. Starting in 3.2, however, every accepted signature on a request with a body must cover Content-Digest. Agents that don’t support signing may continue using their existing authentication path.
Two signing profiles in AdCP 3.3. This guide’s steps cover the AdCP RFC 9421 profile (adcp/request-signing/v1, keys published through brand.json). That profile is deprecated in 3.3: it keeps working through 3.x and is removed in 4.0. The experimental Web Bot Auth profile (web-bot-auth) replaces it. New implementations should plan for Web Bot Auth; see Web Bot Auth profile below. Its SDK helpers are in progress.
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 header
  • content-type — media type
  • content-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
If any covered component changes after signing, verification fails. For DNS hosts, signers and verifiers MUST remove one trailing root-label dot after IDNA ToASCII processing and before deriving both @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 distinct kid and adcp_use tag:
  • adcp_use: "request-signing" — for signing outbound tool calls and outbound webhooks
  • adcp_use: "webhook-signing" — deprecated; still accepted on the webhook path for backward compatibility
For blast-radius isolation, publish a second 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:
The verifier calls your agent, not your domain’s /.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

This generates an Ed25519 keypair and writes:
  • private.jwk — the private key (JWK with d field). Keep this secret.
  • public-jwks.json — the public key in JWKS format. Publish this.

Programmatic

Supported algorithms

The algorithm name differs between the JWK entry ("alg": "EdDSA") and the RFC 9421 Signature-Input parameter (alg="ed25519"). See the algorithm naming table in the spec.

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 createKmsSigner for 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 0600 and O_EXCL so an existing file is never overwritten — Path.write_bytes inherits the process umask (often 0644, 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):
Only public keys go here — no 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’s get_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

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:
This avoids sending signatures to agents that don’t expect them and caches capability lookups.

Step 4: Verify inbound signatures (seller)

Framework middleware

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 with tag="adcp/webhook-signing/v1" and Content-Digest always covered (no opt-out).

Step 6: Sign outbound webhooks (seller)

Pass a signerKey to createAdcpServer and the framework signs every outbound webhook automatically:
Publish the webhook signing public key as a JWK with "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, declare signed_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.
Buyers call 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:
  1. Give each agent its own origin. An agency hosting several agents on one host moves each to a subdomain.
  2. Serve the key directory at that origin, as Key directories specifies. Regenerate it before its signatures expire.
  3. Sign each request and webhook as Profile rules specifies, for sellers that advertise web-bot-auth in request_signing.profiles (added by #7894).
To be bound by a seller:
  1. 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.
  2. List the origin in https://{registrable domain}/.well-known/trust.json with its role (adcp:buying for a buyer). The record carries no keys.
Verifier setup:
  1. Verify signatures with the profile’s Verification procedure, and answer failures as its Error responses section says.
  2. 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 open onboarding mode.
  3. Once you have bound an origin, refuse RFC 9421 signatures for it.
  4. Advertise web-bot-auth in request_signing.profiles (added by #7894).
For the end-to-end buyer flow, see A buyer agent onboards itself. For moving from the RFC 9421 profile, see the agent identity migration guide.

Key rotation

The JWKS endpoint supports multiple keys simultaneously for zero-downtime rotation:
  1. Generate a new keypair with a new kid
  2. Add the new public key to JWKS (both old and new are published)
  3. Update signing configuration to use the new private key
  4. After 24–48 hours, remove the old public key from JWKS
For emergency rotation (key compromise), add the old 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-digest covered (except the one negative that tests its absence) and assume a verifier advertising covers_content_digest: "required". The set also includes rejections of malformed RFC 8941 sf-binary and malformed authorities.

Grade your verifier

Error codes

When verification fails, return 401 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.