Spend a batch preview token and commit its writes
POST /batch/asset-fields/apply
Commits the would_change subset of a preview. The body has
EXACTLY THREE MEMBERS — token, reason and a confirm_count
that is required for overwrite and remove and forbidden
otherwise. It carries NO mode and NO value: both are read from
the token, which is why no payload-mismatch refusal exists.
The token is single-use, and consumption is a COMMITTED
OUTCOME rather than a delivered response
Token consumption, the durable field and vocabulary mutations, and the operation’s single audit envelope are ONE ATOMIC COMMITTED OUTCOME. There are exactly two durable results:
- A PRE-WRITE REFUSAL — anything below — commits no field value, no vocabulary term and no audit envelope, and LEAVES THE TOKEN USABLE. An operator who mistypes the confirmation count or omits the reason corrects the payload and retries without re-previewing.
- A COMMITTED APPLY — including a partial one where some
targets conflicted, and including one where
would_changewas zero — commits its result, EXACTLY ONE audit envelope and the consumption together.
A lost HTTP response therefore never makes a spent token
spendable. Replaying a spent token answers 409
preview_consumed with zero additional writes, zero additional
vocabulary mutations and zero additional audit envelopes.
Validation precedence, and why it is exactly this
NO token-bound fact — the mode, the would_change count, the
field, the target set, the expiry, the consumption state or the
expected confirmation count — may influence any externally
visible response until BOTH integrity and caller binding have
succeeded.
Token-INDEPENDENT request-shape checks may run first, because
they leak nothing: a missing token member (400
token_required), the reason’s bounds (400
reason_payload_too_large / reason_required /
reason_too_long) and a confirm_count outside its CONSTANT
bounds (400 confirm_count_invalid). Then, in exactly this
order:
- integrity — 403
preview_token_invalid - caller binding — 403
preview_token_invalid - consumption state — 409
preview_consumed - expiry — 409
preview_expired - mode-specific confirmation validation — 400
confirm_count_required/confirm_count_not_applicable/confirm_count_mismatch - current authority, definition, configuration, vocabulary and reference-liveness checks — 403 / 409
- the committed apply
MALFORMED, UNKNOWN, TAMPERED and ANOTHER CALLER’S tokens
collapse to ONE BYTE-IDENTICAL 403 preview_token_invalid, in
status, body and message, and stay indistinguishable however
else they differ — expired, consumed, overwrite-mode,
fill_empties-mode. A client MUST NOT be built to tell them
apart, and cannot be: mode-specific confirmation errors are
reachable only by the token’s OWN caller, because apply does not
send the mode and answering confirm_count_required versus
confirm_count_not_applicable before binding would report
somebody else’s token’s mode.
For a token that IS provably the caller’s own, CONSUMED WINS
OVER EXPIRED: a token that is both answers 409
preview_consumed, because that tells the operator their
operation ALREADY RAN, where preview_expired would invite them
to run it a second time.
The operator reason
REQUIRED, and deliberately stricter than the optional reason fields elsewhere in this API: a bulk mutation reaches up to 1000 records and there is no undo. It is NOT bound into the token — it is a fact about this apply, not about the preview — so there is no substitution to detect. Recorded verbatim, after trimming, in the audit envelope.
Concurrency
Batch-wide: a change to the field’s status, read_only,
required, regexp_filter, type, mirrors_column,
applies_to, read_capability or write_capability answers
409 definition_drift with ZERO writes; a change to its
vocabulary options document answers 409 vocabulary_drift; the
proposed reference target ceasing to resolve answers 409
reference_invalidated. Per target: a value that moved since
the preview is conflict, a SOFT-DELETED target is gone, and
a target the caller can no longer reach is
unauthorized_at_apply with a sub-reason. An ARCHIVED asset is
not a state change and is still written — archive is not
deletion, on the subject plane or the reference plane.
The apply reports on completion. There is no live progress.
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”EXACTLY THREE MEMBERS. No mode and no value: the token carries both, which is why no payload-mismatch refusal exists.
object
The opaque single-use token from the preview. Absent → 400
token_required. Malformed, unknown, tampered, or bound to
another caller → ONE byte-identical 403
preview_token_invalid.
REQUIRED. Why this bulk change is being made, recorded verbatim in the audit envelope.
THE AUTHORITATIVE RULE, which the server enforces and this
schema cannot express: the value is TRIMMED, must be
NON-EMPTY after trimming, and must be AT MOST 500 UNICODE
CODE POINTS AFTER TRIMMING. 500 code points, not 500 bytes
— a 500-character reason written in a multi-byte script is
accepted. Over 500 after trimming is 400 reason_too_long;
absent, empty or whitespace-only is 400 reason_required.
The maxLength: 2000 above is a RAW DEFENSIVE CEILING on
the request as received, NOT the product rule. It exists
only to cap request size, it is checked BEFORE trimming,
and exceeding it is a DISTINCT refusal — 400
reason_payload_too_large. It is deliberately looser than
the semantic limit so that a conforming external validator
never rejects a value this server would accept: whitespace
around a 500-code-point body is valid and would be refused
by a schema that encoded 500 here.
The typed confirmation. REQUIRED for overwrite and
remove, FORBIDDEN otherwise — supplying it on
fill_empties or append is REFUSED with 400
confirm_count_not_applicable rather than ignored.
It confirms the token’s would_change, NOT eligible: the
number the operator types is the number of records that
will actually change. A mismatch is 400
confirm_count_mismatch and names the expected value.
Its CONSTANT bounds — an integer, not negative, not above 1000 — are checked before the token is looked at, because they are facts about the schema rather than about anybody’s token. Everything mode-specific about it is checked only AFTER the token has been proved to be this caller’s own.
Responses
Section titled “ Responses ”The apply committed. Its result, its single audit envelope and the token’s consumption are one durable outcome.
A COMMITTED apply. The token is spent, exactly one audit envelope exists for it, and the two facts committed together with the writes.
A result with changed: 0 is still a committed apply — an
operation whose preview found nothing to change is a real
operation, it consumes its token, and it records one envelope.
object
What the batch does to each target’s value.
overwrite— replace the value on every eligible target. Reportsno_opas ZERO even against a target already holding the proposed value: a set advancesset_atand writes a history row, so it is a real change to the record.fill_empties— write only where the value is semantically EMPTY. A target holding any non-empty value isno_op. FALSE is a real boolean value and is never empty.append— add the proposed terms to the existing set.remove— take the proposed terms out of the existing set.
append and remove are multi_select ONLY. Every other type
refuses them batch-wide with 422 mode_not_supported_for_type
rather than being given an invented set semantics.
The preview’s partition counts, as bound into the token.
object
object
Every would_change target and what happened to it.
object
What happened to one would_change target at apply.
would_change = changed + conflict + gone
+ unauthorized_at_apply + error
changed— written.conflict— the value moved since the preview. The rest of the batch proceeds.gone— the target was SOFT-DELETED since the preview. An ARCHIVED target is not gone and is still written.unauthorized_at_apply— the caller’s effective permission over this target changed. Carries a sub-reason.error— an unexpected per-target failure.
Vocabulary terms this apply actually created. A term is
minted ONLY IF at least one target successfully stored it;
if every would_change target ended conflict, gone,
unauthorized_at_apply or error, the options document is
left byte-identical and this is empty.
The request could not have been valid for any state of the system.
ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.
Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.
expected and actual are populated where a number is the
whole story: the count a confirm_count_mismatch wanted, and
the ceiling and the measured size on the two ceiling refusals.
field is the field’s code where one is known.
object
Why a batch preview or apply was refused. The status code carries the class and this carries the cause.
400 — the request could not have been valid for ANY state:
empty_selection, unknown_mode, unknown_selection_kind,
value_type_mismatch, token_required,
reason_payload_too_large, reason_required,
reason_too_long, confirm_count_invalid,
confirm_count_required, confirm_count_not_applicable,
confirm_count_mismatch.
403 — authorization, INCLUDING anything not provably this
caller’s token: preview_token_invalid,
bulk_capability_required, field_write_capability_required,
vocabulary_extend_required.
422 — well formed, understood, and refused on its merits:
field_archived, field_read_only, field_mirrored,
mode_not_supported_for_type, pattern_mismatch,
required_value_empty, fill_empties_value_empty,
unknown_slug, archived_slug, dangling_reference,
selection_entry_ceiling, expanded_target_ceiling.
409, apply only — the token is provably this caller’s own but
is spent or stale, or the world moved; the remedy is to
re-preview: preview_consumed, preview_expired,
definition_drift, vocabulary_drift, reference_invalidated.
dangling_reference (preview) and reference_invalidated
(apply) are deliberately NOT the same code. The first says the
proposed target never resolved; the second says it resolved
when the operator looked and has since stopped. They call for
different actions and a client that cannot tell them apart
cannot say which.
On expanded_target_ceiling, the number of SELECTION
ENTRIES that expanded to the over-ceiling target count — so
an operator can see that a handful of posts reached
thousands of assets.
Authentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}Not provably this caller’s own token, or the caller’s current authority does not permit the operation.
ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.
Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.
expected and actual are populated where a number is the
whole story: the count a confirm_count_mismatch wanted, and
the ceiling and the measured size on the two ceiling refusals.
field is the field’s code where one is known.
object
Why a batch preview or apply was refused. The status code carries the class and this carries the cause.
400 — the request could not have been valid for ANY state:
empty_selection, unknown_mode, unknown_selection_kind,
value_type_mismatch, token_required,
reason_payload_too_large, reason_required,
reason_too_long, confirm_count_invalid,
confirm_count_required, confirm_count_not_applicable,
confirm_count_mismatch.
403 — authorization, INCLUDING anything not provably this
caller’s token: preview_token_invalid,
bulk_capability_required, field_write_capability_required,
vocabulary_extend_required.
422 — well formed, understood, and refused on its merits:
field_archived, field_read_only, field_mirrored,
mode_not_supported_for_type, pattern_mismatch,
required_value_empty, fill_empties_value_empty,
unknown_slug, archived_slug, dangling_reference,
selection_entry_ceiling, expanded_target_ceiling.
409, apply only — the token is provably this caller’s own but
is spent or stale, or the world moved; the remedy is to
re-preview: preview_consumed, preview_expired,
definition_drift, vocabulary_drift, reference_invalidated.
dangling_reference (preview) and reference_invalidated
(apply) are deliberately NOT the same code. The first says the
proposed target never resolved; the second says it resolved
when the operator looked and has since stopped. They call for
different actions and a client that cannot tell them apart
cannot say which.
On expanded_target_ceiling, the number of SELECTION
ENTRIES that expanded to the over-ceiling target count — so
an operator can see that a handful of posts reached
thousands of assets.
The token is provably this caller’s own but is spent or stale, or the world moved. The remedy is to re-preview.
ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.
Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.
expected and actual are populated where a number is the
whole story: the count a confirm_count_mismatch wanted, and
the ceiling and the measured size on the two ceiling refusals.
field is the field’s code where one is known.
object
Why a batch preview or apply was refused. The status code carries the class and this carries the cause.
400 — the request could not have been valid for ANY state:
empty_selection, unknown_mode, unknown_selection_kind,
value_type_mismatch, token_required,
reason_payload_too_large, reason_required,
reason_too_long, confirm_count_invalid,
confirm_count_required, confirm_count_not_applicable,
confirm_count_mismatch.
403 — authorization, INCLUDING anything not provably this
caller’s token: preview_token_invalid,
bulk_capability_required, field_write_capability_required,
vocabulary_extend_required.
422 — well formed, understood, and refused on its merits:
field_archived, field_read_only, field_mirrored,
mode_not_supported_for_type, pattern_mismatch,
required_value_empty, fill_empties_value_empty,
unknown_slug, archived_slug, dangling_reference,
selection_entry_ceiling, expanded_target_ceiling.
409, apply only — the token is provably this caller’s own but
is spent or stale, or the world moved; the remedy is to
re-preview: preview_consumed, preview_expired,
definition_drift, vocabulary_drift, reference_invalidated.
dangling_reference (preview) and reference_invalidated
(apply) are deliberately NOT the same code. The first says the
proposed target never resolved; the second says it resolved
when the operator looked and has since stopped. They call for
different actions and a client that cannot tell them apart
cannot say which.
On expanded_target_ceiling, the number of SELECTION
ENTRIES that expanded to the over-ceiling target count — so
an operator can see that a handful of posts reached
thousands of assets.
Well formed and understood, and refused on its merits.
ONE refusal body for both batch operations, so the two cannot describe the same refusal differently.
Every apply-time refusal — the request-shape checks and steps 1 through 6 alike — writes zero field values, mints no vocabulary term, commits no audit envelope, and LEAVES THE TOKEN UNSPENT.
expected and actual are populated where a number is the
whole story: the count a confirm_count_mismatch wanted, and
the ceiling and the measured size on the two ceiling refusals.
field is the field’s code where one is known.
object
Why a batch preview or apply was refused. The status code carries the class and this carries the cause.
400 — the request could not have been valid for ANY state:
empty_selection, unknown_mode, unknown_selection_kind,
value_type_mismatch, token_required,
reason_payload_too_large, reason_required,
reason_too_long, confirm_count_invalid,
confirm_count_required, confirm_count_not_applicable,
confirm_count_mismatch.
403 — authorization, INCLUDING anything not provably this
caller’s token: preview_token_invalid,
bulk_capability_required, field_write_capability_required,
vocabulary_extend_required.
422 — well formed, understood, and refused on its merits:
field_archived, field_read_only, field_mirrored,
mode_not_supported_for_type, pattern_mismatch,
required_value_empty, fill_empties_value_empty,
unknown_slug, archived_slug, dangling_reference,
selection_entry_ceiling, expanded_target_ceiling.
409, apply only — the token is provably this caller’s own but
is spent or stale, or the world moved; the remedy is to
re-preview: preview_consumed, preview_expired,
definition_drift, vocabulary_drift, reference_invalidated.
dangling_reference (preview) and reference_invalidated
(apply) are deliberately NOT the same code. The first says the
proposed target never resolved; the second says it resolved
when the operator looked and has since stopped. They call for
different actions and a client that cannot tell them apart
cannot say which.
On expanded_target_ceiling, the number of SELECTION
ENTRIES that expanded to the over-ceiling target count — so
an operator can see that a handful of posts reached
thousands of assets.