Problem
A seller answering a brief has two structured outcomes available: products, or an error. There is no way to say "we are declining this brief, here is why, and here is what would change the answer."
That third outcome is ordinary commerce. A publisher declines because the budget is below what they will sell the inventory for, because they do not carry the channel that was asked for, or because the flight is sold out. None of those are errors — the request was well-formed, the seller understood it, and the seller made a commercial decision.
Today the only compliant home is the error channel. Per the GOVERNANCE_DENIED wire-placement rule, a task whose response defines no rejection arm puts the denial in errors[] + adcp_error and does flip transport failure markers (HTTP 4xx, MCP isError: true). So a declined brief is delivered to the buyer as INVALID_REQUEST — indistinguishable, at the dispatch layer, from a malformed request.
Two consequences:
- Buyers cannot dispatch on it. A buyer agent receiving
INVALID_REQUEST should re-examine its own request. The correct response to a decline is the opposite: change the commercial terms and come back. Agents that treat 4xx as a bug will not negotiate, and agents that retry unchanged will get the same answer.
- Sellers who want to disclose have nowhere good to put it. "Your CPM is below what we can sell this inventory for — come back higher" is a negotiation move that invites a better bid. Sending it as the message on an
INVALID_REQUEST envelope means many buyer agents will never surface it.
There is also a confidentiality hazard, which is what brought us here. With no structured place for a sanitized decline, the path of least resistance for an implementer is to pipe the composer's internal explanation into the error message — the merchandising rule that fired, the internal inventory ids it excluded, the signal vendor that was not live. That is seller-confidential material crossing to the counterparty in a negotiation. We found and fixed exactly that leak in our own implementation. A protocol that offers no sanitized decline shape makes the leaky version the default one.
The spec already wants this
Four observations, all from the published 3.1 surface:
1. task-status.json reserves rejected, and no arm gives it a payload. TaskStatus is 'submitted' | 'working' | 'input-required' | 'completed' | 'canceled' | 'failed' | 'rejected' | 'auth-required' | 'unknown'. The completed-response shapes type status as the full enum, so a seller can emit status: 'rejected' — but no schema defines what accompanies it. There is no reason, no suggestions, and no normative guidance. Two sellers emitting a rejected get_products today would produce two different payloads, and no buyer could rely on either. An enum value with no payload contract is an interop hazard, not a feature.
2. The doctrine is already written — only the arms are missing. From the GOVERNANCE_DENIED guidance:
"Governance denial is a structured business outcome, not a system error — the governance call SUCCEEDED and the agent returned a denial verdict… Transport-level success markers MUST NOT be flipped (HTTP 200, MCP isError: false, A2A succeeded) — the task ran successfully and produced a structured response… The rule generalizes to any current or future task whose response defines a discriminated rejection arm."
The general rule exists. Only two tasks have an arm to apply it to.
3. AcquireRightsRejected already solved the confidentiality problem. Its reason is documented as:
"Why the rights request was rejected. May be sanitized to protect confidential brand rules — e.g., 'This violates our public figures brand guidelines' rather than naming the specific rule."
and its suggestions:
"Actionable alternatives the buyer can try. If present, the rejection is fixable — the buyer can adjust their request. If absent, the rejection is final."
That is precisely the shape a declined brief needs, and the fixable/final signal is exactly what a buyer agent must branch on. CreativeRejected carries the same pair. The pattern is proven; it is just not available on the discovery surface.
4. get_products is already a status-discriminated union. GetProductsResponse | GetProductsAsyncWorking | GetProductsAsyncInputRequired | GetProductsAsyncSubmitted, discriminated by the task-level status literal. Adding a rejected arm extends an existing union rather than introducing discrimination — no new dispatch mechanism, and no conflict with the oneOf-discriminator audit (#3917, #3939).
Related but distinct: #3299 (normalize Submitted-arm coverage) is the same class of gap on the async side, and names get_products explicitly. This is its sibling on the rejection side. #5540 (negotiated pricing, closed COMPLETED) covers the case where the buyer asked for a quote; a decline on an unsolicited brief is upstream of that handshake and is not covered by it.
Proposal
Add GetProductsRejected to the get_products response union:
interface GetProductsRejected {
// Task-level discriminator. Already a legal task-status value; this arm
// gives it a payload contract for the first time.
status: 'rejected'
// Why the brief was declined, in market terms. MAY be sanitized to protect
// confidential merchandising rules — "your budget is below what we can sell
// this inventory for" rather than naming the rule, the internal inventory
// id, or the data vendor. Plain text only.
//
// Buyers MUST treat this as untrusted seller input: escape before rendering
// to HTML, and sanitize or isolate before passing into an LLM prompt context.
// (Same warning already carried on GetProductsAsyncSubmitted.message.)
reason: string
// Actionable alternatives: raise the budget, other channels, other flight
// dates. If present, the decline is fixable and the buyer can re-brief.
// If absent, it is final for this brief.
suggestions?: string[]
context?: ContextObject
ext?: ExtensionObject
}
Normative rules, all inherited from the existing rejection arms:
- Sellers using this arm MUST NOT also emit
adcp_error / errors[] (mirror the not: { required: [errors] } constraint that AcquireRightsRejected and CreativeRejected already declare).
- Transport-level success markers MUST NOT be flipped: HTTP 200, MCP
isError: false, A2A succeeded.
- Buyers MUST dispatch on
status first and fall back to adcp_error.code only when no rejection arm was used — the existing two-layer dispatch rule, unchanged.
Boundaries between adjacent shapes
Worth writing down explicitly, because these are currently conflated in the wild:
| Situation |
Shape |
| Catalogue queried, nothing matched the filters; seller takes no position |
status: 'completed', products: [] |
| Seller understood the brief and is declining it |
status: 'rejected' + reason |
| Request was malformed, unauthorized, or unsupported |
adcp_error, failure markers flipped |
| Seller needs time or a human before answering |
status: 'submitted' / 'working' (#3299) |
| Buyer asked for a quote and is negotiating price |
proposal / quote handshake (#5540) |
An empty products: [] is not a substitute for a decline: it is indistinguishable from "your filters excluded everything" and gives the buyer nothing to branch on. An envelope-level message is not a substitute either — it is prose alongside an outcome, not an outcome.
Generalization
The arm shape is task-agnostic. The same { status: 'rejected', reason, suggestions? } triple applies wherever a seller declines a well-formed business ask — create_media_buy being the obvious next one, which today can only express a denial through the GOVERNANCE_DENIED case-2 path. We suggest landing it on get_products first, where declines are most frequent and least dangerous to get wrong, then generalizing.
Implementation experience
We implemented seller-controlled declines on a composition storefront: the publisher's merchandising rulebook decides whether a buyer is told why a brief did not land, the agent writes that decline in the seller's voice, and a mechanical screen drops any wording that quotes the rulebook or names an internal id before it can reach the buyer. The behaviour we wanted was already fully described by AcquireRightsRejected's reason + suggestions semantics.
The only thing we could not do was put it in the right envelope. It ships as adcp_error: INVALID_REQUEST with a sanitized message, because that is what the spec prescribes for a task with no rejection arm — which means a well-formed brief, correctly understood and deliberately declined, is labelled to the buyer as an invalid request.
Problem
A seller answering a brief has two structured outcomes available: products, or an error. There is no way to say "we are declining this brief, here is why, and here is what would change the answer."
That third outcome is ordinary commerce. A publisher declines because the budget is below what they will sell the inventory for, because they do not carry the channel that was asked for, or because the flight is sold out. None of those are errors — the request was well-formed, the seller understood it, and the seller made a commercial decision.
Today the only compliant home is the error channel. Per the
GOVERNANCE_DENIEDwire-placement rule, a task whose response defines no rejection arm puts the denial inerrors[]+adcp_errorand does flip transport failure markers (HTTP 4xx, MCPisError: true). So a declined brief is delivered to the buyer asINVALID_REQUEST— indistinguishable, at the dispatch layer, from a malformed request.Two consequences:
INVALID_REQUESTshould re-examine its own request. The correct response to a decline is the opposite: change the commercial terms and come back. Agents that treat 4xx as a bug will not negotiate, and agents that retry unchanged will get the same answer.INVALID_REQUESTenvelope means many buyer agents will never surface it.There is also a confidentiality hazard, which is what brought us here. With no structured place for a sanitized decline, the path of least resistance for an implementer is to pipe the composer's internal explanation into the error message — the merchandising rule that fired, the internal inventory ids it excluded, the signal vendor that was not live. That is seller-confidential material crossing to the counterparty in a negotiation. We found and fixed exactly that leak in our own implementation. A protocol that offers no sanitized decline shape makes the leaky version the default one.
The spec already wants this
Four observations, all from the published 3.1 surface:
1.
task-status.jsonreservesrejected, and no arm gives it a payload.TaskStatusis'submitted' | 'working' | 'input-required' | 'completed' | 'canceled' | 'failed' | 'rejected' | 'auth-required' | 'unknown'. The completed-response shapes typestatusas the full enum, so a seller can emitstatus: 'rejected'— but no schema defines what accompanies it. There is noreason, nosuggestions, and no normative guidance. Two sellers emitting a rejectedget_productstoday would produce two different payloads, and no buyer could rely on either. An enum value with no payload contract is an interop hazard, not a feature.2. The doctrine is already written — only the arms are missing. From the
GOVERNANCE_DENIEDguidance:The general rule exists. Only two tasks have an arm to apply it to.
3.
AcquireRightsRejectedalready solved the confidentiality problem. Itsreasonis documented as:and its
suggestions:That is precisely the shape a declined brief needs, and the fixable/final signal is exactly what a buyer agent must branch on.
CreativeRejectedcarries the same pair. The pattern is proven; it is just not available on the discovery surface.4.
get_productsis already astatus-discriminated union.GetProductsResponse | GetProductsAsyncWorking | GetProductsAsyncInputRequired | GetProductsAsyncSubmitted, discriminated by the task-levelstatusliteral. Adding a rejected arm extends an existing union rather than introducing discrimination — no new dispatch mechanism, and no conflict with theoneOf-discriminator audit (#3917, #3939).Related but distinct: #3299 (normalize
Submitted-arm coverage) is the same class of gap on the async side, and namesget_productsexplicitly. This is its sibling on the rejection side. #5540 (negotiated pricing, closed COMPLETED) covers the case where the buyer asked for a quote; a decline on an unsolicited brief is upstream of that handshake and is not covered by it.Proposal
Add
GetProductsRejectedto theget_productsresponse union:Normative rules, all inherited from the existing rejection arms:
adcp_error/errors[](mirror thenot: { required: [errors] }constraint thatAcquireRightsRejectedandCreativeRejectedalready declare).isError: false, A2Asucceeded.statusfirst and fall back toadcp_error.codeonly when no rejection arm was used — the existing two-layer dispatch rule, unchanged.Boundaries between adjacent shapes
Worth writing down explicitly, because these are currently conflated in the wild:
status: 'completed',products: []status: 'rejected'+reasonadcp_error, failure markers flippedstatus: 'submitted'/'working'(#3299)An empty
products: []is not a substitute for a decline: it is indistinguishable from "your filters excluded everything" and gives the buyer nothing to branch on. An envelope-levelmessageis not a substitute either — it is prose alongside an outcome, not an outcome.Generalization
The arm shape is task-agnostic. The same
{ status: 'rejected', reason, suggestions? }triple applies wherever a seller declines a well-formed business ask —create_media_buybeing the obvious next one, which today can only express a denial through theGOVERNANCE_DENIEDcase-2 path. We suggest landing it onget_productsfirst, where declines are most frequent and least dangerous to get wrong, then generalizing.Implementation experience
We implemented seller-controlled declines on a composition storefront: the publisher's merchandising rulebook decides whether a buyer is told why a brief did not land, the agent writes that decline in the seller's voice, and a mechanical screen drops any wording that quotes the rulebook or names an internal id before it can reach the buyer. The behaviour we wanted was already fully described by
AcquireRightsRejected'sreason+suggestionssemantics.The only thing we could not do was put it in the right envelope. It ships as
adcp_error: INVALID_REQUESTwith a sanitized message, because that is what the spec prescribes for a task with no rejection arm — which means a well-formed brief, correctly understood and deliberately declined, is labelled to the buyer as an invalid request.