Skip to content

[Change] § 2, § 5.5, § 5.6 — One error object for rejections: code, pointer, current version, retry #130

Open

Description

@KonstantinMirin

What kind of change?

Change a normative requirement (MUST / SHOULD / MAY)

Which section?

Whole document / more than one section

Current text

§ 5.6.1: A request carrying an `idempotency_key` already seen MUST return the
original result and MUST NOT create a second commitment.

§ 5.6.1, as PR #73 proposes: The rejection MUST return the current version
number, so the buyer can fetch it again. […] Reusing an `idempotency_key` with
different commitment terms MUST produce an explicit conflict […]

§ 5.5, as PR #71 proposes: A seller MUST reject it deterministically rather
than ignore it.

§ 5.6.3, as PR #70 proposes: A seller MUST reject a commitment that includes a
line item whose `requires` is unmet.

Proposed text

§ 2, add a subsection:

### Errors

A party that rejects a request MUST return `errors[]`, one or more error
objects. This specification defines the object. The binding that carries the
request carries the errors. Once an AAMP wire contract defines an error
object, this subsection cites it and keeps only the codes.

A seller's decline of a resolved `commitment`, and a refused hold, are answers
to a valid request, not errors.

| Key | Required | Meaning |
| :--- | :--- | :--- |
| `code` | REQUIRED | A core code (below) or an extension code `<domain>:<code>` |
| `detail` | REQUIRED | Human-readable. A receiver SHOULD NOT parse it |
| `pointer` | OPTIONAL | JSON Pointer (RFC 6901), into the object this specification defines that the request carries, to the value that caused the error |
| `version` | REQUIRED when the request names a proposal that exists | The proposal's `version` when the error was decided |
| `retry` | REQUIRED | `same`: the same request, with the same `idempotency_key`, can succeed later; such an error is not a stored result (§ 5.6.1). `refetch`: a new attempt, composed on the current `version` with a new `idempotency_key`, can succeed. `none`: only a changed request, with a new `idempotency_key`, can succeed |

Core codes, which IAB Tech Lab maintains apart from the schema:

| `code` | Returned when |
| :--- | :--- |
| `validation` | A value breaks a rule of this specification |
| `not_found` | A named proposal or line item does not exist |
| `not_offered` | A buyer value is outside what the seller declared (§ 3.1, § 3.2) |
| `unresolved_reference` | A `catalog_ref` (§ 2.2) or a hash-pinned reference does not resolve, or its content does not match its hash |
| `not_committable` | A line item's `status`, or a `requires` that is unmet, rules out commitment |
| `stale_version` | The commitment was composed on terms that are not the current terms (§ 5.6.1) |
| `expired` | `valid_until` or `price_valid_until` has passed |
| `idempotency_conflict` | An `idempotency_key` is reused with different terms |

Anyone MAY define an extension code. Its prefix is a domain the definer
controls. A code's meaning MUST NOT change once published. A receiver that
does not recognize a code, or a key of the error object, acts on `retry`.

Informative: over HTTP, `validation`, `not_offered`, and
`unresolved_reference` fit 400 Bad Request; `not_found` 404 Not Found;
`not_committable` and `stale_version` 409 Conflict; `expired` 410 Gone;
`idempotency_conflict` 422 Unprocessable Content. An error with
`retry: same` fits 503 Service Unavailable.

§ 5.6.1 (PR #73): replace "The rejection MUST return the current version
number" with "The rejection MUST carry `stale_version`, or `expired` when
`valid_until` or `price_valid_until` has passed", and "MUST produce an
explicit conflict" with "MUST return `idempotency_conflict`".
§ 5.5 (PR #71): an exclusion type the seller did not accept returns
`not_offered`; an entry whose platform it cannot determine, `validation`.
§ 5.6.3 (PR #70): an unmet `requires` returns `not_committable`.

Why

Open PRs require a seller to reject four cases, and none says how. We ask for one error object, so a buyer agent can act on a rejection without reading prose.

A stale commitment comes from @Kishore-2572's commitment version PR (#73). An unaccepted exclusion type and an undeterminable platform come from @Sirajmx's block-list PR (#71), and an unmet requires from @Sirajmx's line-item dependencies PR (#70). The draft at commit 44dfaaf never uses the word "error". #73 requires the current version in a rejection, with no field for it.

We revise our replay notes on #24, which named the behavior and not the code: an agent can't reliably act on prose.

Others asked for this. @VJAlkimi's format validation change request (#32) proposes { field, id, reason, detail }, one shape for both #32 and @Sirajmx's block-list change request (#25). @unreconciled reuses it on #5. @aleksUIX on #25 and @fgranata on #25 ask for "a structured error", and @EvgenyAndroid (#128) for a conflict error. We build on #32's shape: field becomes pointer, reason becomes code, and pointer replaces id. We add version and retry.

The shape follows the OpenDirect 2.1 error response: errors, an array of objects with a symbolic ErrorCode. OpenDirect lists no codes. detail and pointer come from RFC 9457 problem details: clients SHOULD NOT parse detail, and its validation example points with a JSON Pointer. Core codes reuse the iab-agentic-primitives ErrorCode names where meanings match. OpenRTB 3.0 no-bid reasons reserve 500 and above for exchanges, so two vendors can pick the same number. A domain prefix can't collide, as in our cost-method change request (#94).

retry borrows its idea from gRPC's status code guidance: retry the call (same), refetch and recompose (refetch), or change the request (none). The reference buyer agent retries only on a timeout or HTTP 502, 503, or 504. A stale version needs a refetch, not a wait.

OpenProposal isn't tied to HTTP, and the primitives also send errors inside a JSON-RPC result. So the binding carries the object (@dineshbhat's operations comment (#113)), and the HTTP statuses are informative.

A decline isn't an error. A seller's decline (our commitment acceptance change request (#67)) and a refused hold (our hold-states change request (#66)) answer a valid request.

Details: Error model: full rationale and examples

Knock-on effects

Open questions for the working group

  1. Which AAMP document should carry the error object, with OpenProposal keeping only its codes? Our scope comment (#59) proposes that wire contract for § 1.2, and @dineshbhat's operations comment (#113) asks for it. The primitives' error envelope covers that layer, with a closed code list.
  2. Should stale_version and expired take the primitives' names, contention and quote_expired?
  3. Should a decline under #67 or a refused hold under #66 carry error objects as its reason? OpenDirect 2.1 gives a declined Line a free-text statechangereason. Open question 5 in our commitment-set comment on #23 asks the same about a rejected set.
  4. Should the core list cover cases the draft implies but doesn't state, such as a buyer outside § 5.8 Transaction eligibility?
  5. Should pointer be a JSON Pointer, as RFC 9457 uses, or a JSONPath, as the primitives' unsupported[].path writes it?

Would this break an implementation already built to this draft?

No — additive or editorial

Alternatives you considered and rejected

  • RFC 9457 problem details as the object. Its status member must match the HTTP response. Over HTTP, a binding can still send our object as problem details.
  • Integer codes with a vendor range (OpenRTB 3.0). Two vendors can pick the same code.
  • A boolean retryable. It can't tell a wait from a refetch.
  • Prose only, as PR § 4.1, § 5.6.1 — Commitments carry proposal_id and version (#24) #73 drafts it. An agent can't reliably branch on it.

You are commenting as

Other — IAB Tech Lab member (Postindustria).

Before you submit

  • I have read CONTRIBUTING.md and agree my contribution may be published under this repository's licence.
  • This contains no confidential or commercially sensitive information.

Revised 2026-10-27: Knock-on effects add @EvgenyAndroid's catalog digest feedback (#129): a digest mismatch is unresolved_reference.

Analyzed and generated with the help of Claude, fully reviewed by me.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions