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, and the open PRs that add rejections:

§ 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 […] Reusing an `idempotency_key` with a different body 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

Our errors and extensions PR carries § 2.4, quoted here without its informative HTTP mapping, and the § 5.5 and § 5.6.3 codes. Our signing PR words idempotency_conflict against a Commit and orders the § 5.6.1 codes. The § 4.3.2 and § 5.8 codes land with our lifecycle and parties PRs.

### 2.4 Errors

A party that rejects a request MUST return `errors[]`, one or more
`ErrorObject`s. This specification defines the object. The message binding that
carries the request carries the errors.

A seller's decline of a resolved `commitment`, or of a hold, answers a valid
request and is not an error. What a party answers when a request fails message
authentication is § 2.8's to state.

**`ErrorObject`** keys:

| Key | Type | Required | Meaning |
| :--- | :--- | :--- | :--- |
| `code` | VocabularyTerm | REQUIRED | A core code (below) or an extension code |
| `detail` | String | REQUIRED | Human-readable. A receiver SHOULD NOT parse it |
| `pointer` | FieldPointer | OPTIONAL | Into the object this specification defines that the request carries, to the value that caused the error |
| `version` | Integer | REQUIRED when the request names a proposal the requester may read | The proposal's `version` when the error was decided |
| `retry` | Enum | REQUIRED | `same` \| `refetch` \| `none`. `same`: the same request can succeed later. `refetch`: a new attempt, composed on the current `version`, can succeed. `none`: only a changed request can succeed. On a `commit`, `same` keeps the `idempotency_key` and such an error is not a stored result (§ 5.6.1), and `refetch` and `none` take a new `idempotency_key` |

Core codes: `validation` \| `not_found` \| `not_offered` \| `unresolved_reference` \|
`not_committable` \| `stale_version` \| `expired` \| `idempotency_conflict` \|
`not_eligible` \| `record_mismatch`.

| `code` | Returned when |
| :--- | :--- |
| `validation` | A value breaks a rule of this specification and no other code applies |
| `not_found` | A named proposal or line item does not exist, or the requester may not read it (§ 5.8) |
| `not_offered` | A buyer value is outside what the seller declared (§ 3.1, § 3.2, § 4.3.2, § 5.3, § 5.5, § 5.6, § 5.6.1), or a buyer asks for a hold where `hold_terms` offers none (§ 5.6.2) |
| `unresolved_reference` | A `DocumentRef` (§ 2.2) does not resolve, or its content does not match its `digest` |
| `not_committable` | A line item's `status` rules out an ask (§ 4.3.2), a `requires` that is unmet rules out a `commit` (§ 5.6.3), a live `commitment` entry already names a line item (§ 4.1.2), or a seller rejects a `commit` before `offer_window.start` (§ 5.6.1) |
| `stale_version` | An ask is stale (§ 4.1.3) |
| `expired` | `offer_window.end` or `price_valid_until` has passed |
| `idempotency_conflict` | An `idempotency_key` is reused with a different `Commit` (§ 5.6.1) |
| `not_eligible` | The buyer or the sender fails an eligibility or authority check the seller applies (§ 5.8) |
| `record_mismatch` | A `record_digest` in the request does not equal the one the receiver recomputes, and § 5.6.1 gives no other code (§ 4.1.1) |

An `ErrorObject` is open. A receiver that does not recognize a code, or a key of
the object, acts on `retry`.
§ 5.6.1: The rejection MUST carry `expired` when `offer_window.end` or
`price_valid_until` has passed, `stale_version` when the `commit` is stale,
`not_committable` when a ground of that code applies (§ 2.4), and otherwise
`record_mismatch`.

§ 5.5: A seller MUST reject it deterministically rather than ignore it, with
`not_offered`.

§ 5.6.3: An unmet `requires` returns `not_committable`.

Why

Open PRs require rejections, and none says what a rejection holds. One error object lets a buyer agent act on a rejection without reading prose.

The cases come from @Kishore-2572's commitment version PR (#73), @Sirajmx's block-list PR (#71), and @Sirajmx's line-item dependencies PR (#70). The public draft never uses the word "error". #73 requires the current version in a rejection, with no field for it.

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

The object follows the OpenDirect 2.1 error response: errors, an array of objects with a symbolic code. detail and pointer come from RFC 9457 problem details. Core codes reuse the iab-agentic-primitives ErrorCode names where meanings match. code is a VocabularyTerm (our shared types PR), so an extension code carries its definer's domain.

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

The binding carries the object, so the HTTP statuses are informative.

Details: Error model: full rationale and examples

Knock-on effects

Open questions for the working group

  1. Answered in part in our post-agreement PR: § 1.2 makes OpenProposal normative for what a request or response carries, such as errors[]. It leaves the envelope and binding to the AAMP wire and lifecycle contract (provisional name). Still open: should that contract define the error object instead, with OpenProposal keeping only its codes? @dineshbhat's operations comment (#113) asks where messages live. 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. Answered in part in our signing PR: a decline entry MUST carry a ground, a VocabularyTerm with the core terms availability, exclusivity, and terms. A decline for lack of availability states its figures in CommitResult.availability. A declined hold under our hold-states change request (#66) carries no reason: hold.status reads declined. OpenDirect 2.1 gives a declined Line a free-text statechangereason. Still open: must a decline state its ground? Our rules issue asks.
  4. Answered in our errors and extensions PR and our parties PR. Should the core list cover cases the draft implies but doesn't state, such as a buyer outside § 5.8 Transaction eligibility? Yes: not_eligible covers a buyer or sender that fails an eligibility or authority check, and record_mismatch a record_digest the seller recomputes differently.
  5. Answered in our shared types PR. Should pointer be a JSON Pointer, as RFC 9457 uses, or a dotted JSON path, as the primitives' unsupported[].path writes it? A JSON Pointer: pointer is a FieldPointer, an RFC 6901 JSON Pointer.
  6. Does an implementation need any of three error rules we left out of the text? One makes an ask, hold, or commit on a shared proposal an error. One makes not_found replace every other code when the requester may not read what a request names. One rejects an availability read on an older version. Our rules issue lists them and offers a PR.

Would this break an implementation already built to this draft?

No — additive or editorial

Alternatives you considered and rejected

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-11-05: A decline states its ground. Rewritten to match § 2.4 in our errors and extensions PR, adding not_eligible and record_mismatch. Questions 4 and 5 are answered, 1 and 3 in part, and 6 is new.

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

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions