Skip to content

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:

  1. 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.
  2. A COMMITTED APPLY — including a partial one where some targets conflicted, and including one where would_change was 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:

  1. integrity — 403 preview_token_invalid
  2. caller binding — 403 preview_token_invalid
  3. consumption state — 409 preview_consumed
  4. expiry — 409 preview_expired
  5. mode-specific confirmation validation — 400 confirm_count_required / confirm_count_not_applicable / confirm_count_mismatch
  6. current authority, definition, configuration, vocabulary and reference-liveness checks — 403 / 409
  7. 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.

EXACTLY THREE MEMBERS. No mode and no value: the token carries both, which is why no payload-mismatch refusal exists.

object
token
required

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.

string
reason
required

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.

string
<= 2000 characters
confirm_count

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.

integer
nullable <= 1000

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
operation_id
required
string format: uuid
mode
required

What the batch does to each target’s value.

  • overwrite — replace the value on every eligible target. Reports no_op as ZERO even against a target already holding the proposed value: a set advances set_at and 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 is no_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.

string
Allowed values: overwrite fill_empties append remove
field_id
required
string format: uuid
field_code
required
string
counts
required

The preview’s partition counts, as bound into the token.

object
expanded
required
integer
eligible
required
integer
would_change
required
integer
no_op
required
integer
refused
required
integer
inapplicable
required
integer
unreadable
required
integer
unauthorized
required
integer
outcome_counts
required
object
changed
required
integer
conflict
required
integer
gone
required
integer
unauthorized_at_apply
required
integer
error
required
integer
targets
required

Every would_change target and what happened to it.

Array<object>
object
asset_id
required
string format: uuid
outcome
required

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.
string
Allowed values: changed conflict gone unauthorized_at_apply error
unauthorized_reason
string
Allowed values: bulk_scope subject_authority unreadable
committed_terms

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.

Array<string>

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
error
required
string
reason
required

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.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

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.

integer
nullable

Authentication required, missing, or invalid

object
error
required

Human-readable error summary

string
Example
the request could not be completed
Example
{
"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
error
required
string
reason
required

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.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

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.

integer
nullable

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
error
required
string
reason
required

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.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

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.

integer
nullable

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
error
required
string
reason
required

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.

string
Allowed values: empty_selection unknown_mode unknown_selection_kind token_required reason_payload_too_large reason_required reason_too_long confirm_count_invalid confirm_count_required confirm_count_not_applicable confirm_count_mismatch preview_token_invalid preview_consumed preview_expired bulk_capability_required field_write_capability_required vocabulary_extend_required field_archived field_mirrored mode_not_supported_for_type required_value_empty fill_empties_value_empty unknown_slug archived_slug selection_entry_ceiling expanded_target_ceiling definition_drift vocabulary_drift reference_invalidated value_type_mismatch field_read_only pattern_mismatch dangling_reference
field
string
nullable
option
string
nullable
expected
integer
nullable
actual
integer
nullable
entry_count

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.

integer
nullable