Skip to main content
AdCP 3.2 proposal negotiation is an immutable-snapshot workflow. Buyers express mechanically verifiable requirements as typed fields and reserve ask for commercial nuance. Sellers validate the complete request before mutation, then return one independently classified result for each source proposal. This guide uses one scenario throughout: Sam, a buyer agent acting for Pinnacle Agency, is negotiating an Acme Outdoor campaign with the StreamHaus sales agent.

Lifecycle

Every transition creates a new proposal_id; the source snapshot remains unchanged. Every successor returned by refine_proposals carries parent_proposal_id equal to the source ID. A buyer can therefore reconstruct negotiation and amendment history by walking the parent chain. draft terms are indicative and do not reserve inventory. finalize copies an unchanged draft into a committed snapshot and creates a hold until expires_at. accept_proposal consumes that hold and creates, updates, or cancels the MediaBuy according to proposal_kind.

Discover what the seller supports

Read get_adcp_capabilities.media_buy.lifecycle_tools before selecting the compact lifecycle. A negotiating seller includes refine_proposals; it may also publish media_buy.proposal_refinement: An explicit supported_dimensions: [] means ask-only refinement. An omitted proposal_refinement block means typed support is unknown, so the buyer must tolerate per-result partial or unable outcomes. When an explicit list omits a dimension, the buyer SHOULD remove or translate that field before sending. If it sends the field anyway, the seller MUST reject the entire task with UNSUPPORTED_FEATURE before creating any successor. Free-text interpretation is competence, not a capability dimension; ask remains available even when the list is empty. When the seller interprets hard targeting found only in ask and that interpretation materially affects eligibility, pricing, or forecasting, inspect the result’s targeting_resolution.brief_targeting. Structured criteria.targeting_overlay is not echoed when accepted exactly; any product-specific departure remains a sparse Product.targeting_resolution.modifications proposal that the buyer must review.

Two failure planes

Do not flatten task errors and negotiation outcomes into one exception type. constraint_unsatisfiable means the seller accepted the dimension but did not satisfy it. unsupported_dimension is used per proposal only when capabilities were unknown; an explicitly omitted dimension is a task-level UNSUPPORTED_FEATURE. commercially_declined applies only to ask, never to a typed constraint. For example, if StreamHaus explicitly omits criteria, the task fails before any proposal is created:

Buyer implementation

1. Build typed-first requests

Sam requests a USD 50,000 ceiling, a CPM ceiling, US/Canada criteria, a product change, and three distinct alternatives. The prose asks for subjective reach optimization only:
Typed fields are hard requirements. Do not copy the legacy get_products.refine[].budget_range shape without changing its meaning: legacy budget_range is a soft discovery preference, while constraints.total_budget forces partial or unable when missed. Legacy more_like_this has no typed compact equivalent and maps only lossily to ask. A fully satisfied revision returns new immutable drafts and no failure prose:

2. Branch on the discriminated result

Handle outcome before reading proposal fields: A valid counteroffer with only two of the three requested alternatives is partial:
An honest refusal that produces no draft uses the same response envelope but no proposal field:

3. Verify terms independently

For every returned proposal:
  1. Recompute sha256:base64url(SHA-256(JCS(commercial_terms))) using RFC 8785 JSON Canonicalization Scheme bytes.
  2. Compare the recomputed digest byte-for-byte with terms_digest.
  3. Verify parent_proposal_id equals source_proposal_id.
  4. Re-evaluate budgets, fixed rates, impression totals, flight bounds, product membership, and structured criteria from commercial_terms.
  5. Verify any targeting_resolution.brief_targeting against the hard targeting in ask, and review every sparse product targeting_resolution.modifications departure before selecting that configured product.
  6. On partial, enforce the partial invariant: every requirement absent from the unsatisfied sets still passes.
  7. Treat reason, description, suggestions, and ask as non-contractual prose.
Digest uniqueness alone is not enough for alternatives. Verify that commercial_terms are distinct after canonicalization; a seller cannot create fake diversity by changing only IDs, names, descriptions, or digest strings.

4. Retry safely

An exact retry uses the same tool name, payload, and idempotency_key; the seller returns the original response with replayed: true. Any payload change requires a new key. Never use a new key merely to extend a committed hold: finalizing the same draft while its hold is active is INVALID_STATE, not a second reservation. A seller may return status: "submitted" with a task_id when a complex multi-publisher revision or inventory hold needs upstream pricing or manual re-underwriting. Poll get_task_status or consume the requested completion notification; do not treat submission as a proposal outcome or retry it under a new key while the task is pending.

5. Finalize, then accept before expiry

Finalization changes no terms:
A finalize batch contains only finalize entries. The seller preflights every source and creates every hold or none. hold_unavailable identifies the source that could not be reserved; otherwise eligible siblings use batch_aborted. The successful result is singular and committed; its commercial terms and digest are unchanged:
Accept the returned committed proposal and its exact digest before expires_at:
The seller atomically verifies the proposal ID, digest, hold expiry, authorization, and any base MediaBuy revision. Expired holds return PROPOSAL_EXPIRED; the buyer requests and finalizes fresh terms rather than retrying acceptance against a lapsed hold.

6. Amend, cancel, or decline

Fork an accepted snapshot with change_kind: "amendment":
When cancellation requires agreement, fork the accepted snapshot with change_kind: "cancellation":
Both successors must be finalized and accepted. Direct control_media_buy cancellation is reserved for rights already granted by the accepted terms. When Sam stops pursuing a draft instead, decline_proposals records terminal feedback; it does not mutate the proposal into another state.

Seller implementation

Preflight the whole request

Before loading an LLM, pricing inventory, or writing state:
  1. Validate the request schema, the maximum of 25 refinements, unique source IDs, and finalize exclusivity.
  2. Authenticate and authorize every source without disclosing cross-account existence.
  3. Compare every typed field with the explicit capability declaration.
  4. Enforce the protocol alternatives ceiling of 10 and any lower advertised max_alternatives.
  5. Look up the idempotency key and reject reuse with a changed payload.
  6. If any task-level check fails, create no successor and no hold.
Capability support promises parse-and-validate competence, not commercial concession. A supported but unacceptable budget is a per-proposal constraint_unsatisfiable result, not UNSUPPORTED_FEATURE.

Keep protocol validation separate from policy

The protocol layer owns schema validation, capability gates, result cardinality, lineage, digest computation, response validation, and transaction boundaries. Application policy owns pricing floors, available product mixes, targeting feasibility, alternative generation, underwriting, and hold duration. Fence ask from pricing and authorization authority. It is attacker-controlled free text and may influence candidate generation only through bounded application policy. Only the typed commercial_terms object becomes contractual.

Stage immutable successors

For revision batches:
  1. Load each immutable source snapshot.
  2. Build candidate commercial_terms without mutating the source.
  3. Evaluate every typed constraint and product action against every candidate.
  4. Classify the ordered result as revised, partial, or unable.
  5. Generate new proposal IDs, set parent_proposal_id, compute JCS digests, and validate the full response.
  6. Commit staged successors only after response validation succeeds.
For partial, include every failed constraint key and failed product action. Every unlisted requirement must pass for every returned draft. constraint_unsatisfiable takes precedence when it coexists with an alternatives shortfall or uninterpreted prose. For finalize batches, acquire holds inside one transaction or equivalent staged operation. If any hold fails, roll back every sibling. A seller may decline a hold, apply its normal credit and relationship checks, and cap concurrent unexpired holds per buyer to resist inventory squatting. Choose expires_at long enough for acceptance but short enough to prevent free inventory options. Return submitted when those checks require asynchronous upstream pricing or manual re-underwriting, preserving the original idempotency scope through terminal completion.

Log for audit without leaking terms

Record the authenticated buyer, source IDs, successor IDs, idempotency fingerprint, capability decision, outcome, unsatisfied keys, hold transaction, and digest. Do not place raw ask, full commercial terms, credentials, or cross-buyer pricing in broadly accessible logs. Rate-limit repeated unheld constraint probes: deterministic failures can otherwise become a price oracle.

Conformance and SDK status

The canonical compliance scenario is media_buy_seller/typed_proposal_negotiation. It exercises capability gating, satisfied and unsatisfied constraints, alternatives, immutable lineage, finalize atomicity, exact replay, acceptance, amendment, cancellation, double-finalize rejection, and multi-source ordering. The deterministic training profiles use the proposal-negotiation primitives published in @adcp/sdk 13.0.0-rc.26. The ordinary /sales/mcp route remains the backward-compatible ask-only seller. Three capability-distinct routes are available to local tests and will become public with the 3.2 beta deployment: The constrained route is the canonical three-to-two exercise: request a USD 50,000 cap, include and omit products, apply US/Canada criteria, and request three alternatives. Retry the returned partial / alternatives_unavailable result with two alternatives before selecting and finalizing one. Exact retries reuse the same idempotency key; a changed alternatives count is a new logical request and needs a new key. For reason-code labs, the semantic ask markers [commercially-declined] and [unsupported-dimension] select those bounded fixture outcomes. An ordinary untyped ask reaches uninterpreted; impossible typed terms reach constraint_unsatisfiable; an unknown source reaches source_unavailable. The training seller caps each principal at three concurrent unexpired holds. Keep the following boundaries in your adapter:
  • capability preflight is separate from mutation;
  • exact replay is separate from changed-request retry;
  • task errors are separate from per-proposal outcomes;
  • commercial policy is separate from protocol verification;
  • finalization is separate from acceptance;
  • immutable terms are separate from explanatory prose.

Implementation checklist

  • Discover lifecycle_tools, supported_dimensions, and max_alternatives.
  • Enforce 25 refinements, 10 alternatives, seller limits, unique sources, and finalize-only batches before mutation.
  • Preserve task errors separately from revised / partial / unable / finalized.
  • Verify typed constraints, product actions, partial unsatisfied subsets, parent lineage, and JCS digests.
  • Reuse a key only for the exact same request; use a new key for changed payloads.
  • Stage successors and validate responses before commit.
  • Create finalize holds atomically and enforce expiry during acceptance.
  • Fork accepted snapshots for amendments and negotiated cancellation.
  • Fence ask, authorize every source, rate-limit probing, and avoid sensitive logs.
  • Run the typed proposal negotiation storyboard against the implementation.