Summary
The requirement to sign a request is triggered by the presence of a nested field inside the request payload. This makes a transport-security decision a function of application-payload semantics at arbitrary depth, and it is the one remaining place where verifier policy cannot be selected before the body is parsed.
AdCP 3.2 established the opposite principle elsewhere in the same document, and the spec already documents the better design for this specific case — so this is a request to finish a migration that is mostly already built, not a new design.
The rule today
docs/building/by-layer/L1/security.mdx (at dc4a454b3):
- :69 — a buyer opts into the legacy HMAC-SHA256 webhook scheme by populating
authentication.credentials at any of four nested locations: push_notification_config, accounts[].notification_configs[], sync_principal.configuration.notification_configs[], sync_agent_notification_configs.notification_configs[].
- :1537 — the error taxonomy makes a signature mandatory when "request payload carries a field that triggers signing regardless of
required_for membership".
So a verifier must read nested payload fields to learn whether the request it is reading was required to be signed.
Note the surface has grown: at v3.1.1 there were two trigger locations; there are now four.
Why this is worth changing
1. It contradicts the principle 3.2 just established. :1469 states that "the endpoint's trusted configuration and negotiated capability select this policy before body parsing or dispatch", and that an operation name or JSON-RPC method inside an unbound body must not drive verifier policy. That reasoning is correct and the webhook-credential escalation is the remaining exception to it.
2. The spec already recommends the replacement. :1631 — "Buyers SHOULD negotiate HMAC-mode out-of-band at onboarding … Durable per-counterparty mode selection in operator records is not MITM-mutable the way a per-request field is."
3. The negotiation surface now exists. 3.2 added legacy_hmac_fallback (:1583) with false documented as the recommended posture. The capability channel this rule needs is already shipped.
The escalation rule at :1537 exists to contain a threat created by expressing the preference in the body in the first place (:1630 — an on-path mutator can strip or inject the block). Removing the per-request trigger removes the threat rather than containing it.
Proposal
Make webhook-signing mode a negotiated, per-counterparty property rather than a per-request payload field:
legacy_hmac_fallback plus the seller's onboarding record become the authoritative selector for HMAC vs 9421 webhook signing.
- Deprecate the per-request
authentication block as a mode trigger on the 3.x timeline it already has (removed in 4.0).
- Once mode no longer comes from the payload, drop the signature escalation at :1537 entirely — it has nothing left to protect.
This is backwards-compatible as a 3.x deprecation: sellers that already set legacy_hmac_fallback: false are unaffected.
Alternative
If the per-request trigger must stay, it would help implementers to state explicitly that a conforming verifier is required to parse the request body before it can determine whether a signature was required, and to say how that interacts with the :1469 "policy before body parsing" rule. Right now the two read as incompatible and implementers have to guess which governs.
Found while implementing a 3.1.1 verifier; re-verified against main at dc4a454b3 before filing.
Summary
The requirement to sign a request is triggered by the presence of a nested field inside the request payload. This makes a transport-security decision a function of application-payload semantics at arbitrary depth, and it is the one remaining place where verifier policy cannot be selected before the body is parsed.
AdCP 3.2 established the opposite principle elsewhere in the same document, and the spec already documents the better design for this specific case — so this is a request to finish a migration that is mostly already built, not a new design.
The rule today
docs/building/by-layer/L1/security.mdx(atdc4a454b3):authentication.credentialsat any of four nested locations:push_notification_config,accounts[].notification_configs[],sync_principal.configuration.notification_configs[],sync_agent_notification_configs.notification_configs[].required_formembership".So a verifier must read nested payload fields to learn whether the request it is reading was required to be signed.
Note the surface has grown: at v3.1.1 there were two trigger locations; there are now four.
Why this is worth changing
1. It contradicts the principle 3.2 just established. :1469 states that "the endpoint's trusted configuration and negotiated capability select this policy before body parsing or dispatch", and that an operation name or JSON-RPC method inside an unbound body must not drive verifier policy. That reasoning is correct and the webhook-credential escalation is the remaining exception to it.
2. The spec already recommends the replacement. :1631 — "Buyers SHOULD negotiate HMAC-mode out-of-band at onboarding … Durable per-counterparty mode selection in operator records is not MITM-mutable the way a per-request field is."
3. The negotiation surface now exists. 3.2 added
legacy_hmac_fallback(:1583) withfalsedocumented as the recommended posture. The capability channel this rule needs is already shipped.The escalation rule at :1537 exists to contain a threat created by expressing the preference in the body in the first place (:1630 — an on-path mutator can strip or inject the block). Removing the per-request trigger removes the threat rather than containing it.
Proposal
Make webhook-signing mode a negotiated, per-counterparty property rather than a per-request payload field:
legacy_hmac_fallbackplus the seller's onboarding record become the authoritative selector for HMAC vs 9421 webhook signing.authenticationblock as a mode trigger on the 3.x timeline it already has (removed in 4.0).This is backwards-compatible as a 3.x deprecation: sellers that already set
legacy_hmac_fallback: falseare unaffected.Alternative
If the per-request trigger must stay, it would help implementers to state explicitly that a conforming verifier is required to parse the request body before it can determine whether a signature was required, and to say how that interacts with the :1469 "policy before body parsing" rule. Right now the two read as incompatible and implementers have to guess which governs.
Found while implementing a 3.1.1 verifier; re-verified against
mainatdc4a454b3before filing.