Skip to content

spec(errors): add PROPOSAL_EXPIRED and PROPOSAL_NOT_FOUND to canonical error catalog #4043

Description

@bokelley

Motivation

Two new error codes needed for the proposal lifecycle (`buying_mode='refine'` with `action='finalize'`):

  • `PROPOSAL_EXPIRED` — `create_media_buy(proposal_id=...)` issued after the committed proposal's `expires_at` hold window has passed. Buyer-fixable: the buyer must re-finalize a fresh proposal. Non-transient.
  • `PROPOSAL_NOT_FOUND` — `create_media_buy` references a `proposal_id` the seller doesn't recognize (typo, wrong tenant, evicted from cache before consumption). Buyer-fixable: re-issue `get_products` to obtain a current proposal.

These are the natural counterparts to existing codes like `MEDIA_BUY_NOT_FOUND`. Today there's no spec-conformant way to signal proposal-lifecycle errors — adopters either reuse `INVALID_REQUEST` (loses semantics, recovery type wrong) or invent local codes (no cross-SDK consistency).

Proposed semantics

`PROPOSAL_EXPIRED`

```yaml
PROPOSAL_EXPIRED:
recovery: terminal
category: proposal-lifecycle
description: |
The proposal's expires_at hold window has passed. Buyer must
re-issue get_products with buying_mode='refine' + action='finalize'
to obtain a fresh committed proposal before calling create_media_buy.
examples:
- "create_media_buy(proposal_id='abc') called 2 hours after the proposal's
expires_at='2026-04-15T14:00:00Z' window expired"
```

`PROPOSAL_NOT_FOUND`

```yaml
PROPOSAL_NOT_FOUND:
recovery: correctable
category: proposal-lifecycle
description: |
The proposal_id is not recognized by the seller — never finalized,
belongs to a different tenant, or evicted from the seller's session
cache. Buyer should re-issue get_products to obtain a current
proposal_id.
examples:
- "create_media_buy(proposal_id='unknown_xyz') without a prior finalize"
- "proposal evicted from session cache before create_media_buy"
```

Wire shape

Same envelope as every other error. Just two new code values in the catalog.

Why now

The Python SDK is shipping v1.5 ProposalManager (PR #538 design doc; implementation pending) which includes finalize-transition handling and `expires_at` enforcement. The framework needs spec-conformant codes for the structured error envelope (especially after PR #525 + PR #536 made structured error codes load-bearing for storyboard `/adcp_error/code` JSON-pointer assertions).

The Python SDK will ship these codes via its `KNOWN_NON_SPEC_CODES` allowlist as a stopgap (same pattern as `CONFIGURATION_ERROR` from spec issue #3995) until the spec catalog includes them. Other SDKs implementing proposal lifecycle support will face the same gap.

Acceptance

  • `PROPOSAL_EXPIRED` and `PROPOSAL_NOT_FOUND` added to `static/schemas/source/enums/error-code.json`'s `enum`, `enumDescriptions`, and `enumMetadata` (per the three-parallel-structures convention)
  • Recovery semantics: `PROPOSAL_EXPIRED` = `terminal`; `PROPOSAL_NOT_FOUND` = `correctable`
  • Targeted for AdCP 3.1
  • Documentation note about how this maps to `buying_mode='refine'` + `action='finalize'` lifecycle

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.enhancementNew feature or requestmedia-buyIssue concerns the media-buy protocol domainneeds-wg-reviewBlocked on a working-group decision — surface in WG meeting agendasschemaJSON Schema source-of-truth: definitions, codegen artifacts, validation, hygiene

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions