Apply Page Layout Operations
POST/v1/:slug/boards/:token/page_layout/operations
Applies up to 50 add / update / move / delete / reorder operations in order and
atomically: one transaction, one lock, one write. The first failure rolls the whole
sequence back, and the error response carries operation_index naming which operation
failed, alongside that operation's own error code — a batch whose third operation misses an
element is still a 404 element_not_found at index 2.
Each operation's keys are exactly the corresponding single endpoint's parameters, plus
type. Unknown keys are rejected, not ignored: an operation with a misspelled argument
would otherwise apply something other than what you asked for. GET /v1/page_layout_elements serves the authoritative argument map under operations.
Ids minted inside a batch are not addressable by later operations of the same batch. An
element you add without an id gets one back only in the response, and a column created by
a section insert is not reported at all. So "add a section, then add a row into its new
column" is only expressible in one batch if you supply the ids yourself — which you
should anyway, since a supplied id is also the idempotency token.
One batch edits one document: subboard_token is a batch-level parameter. Touching the
main page and a nested board's section is two batches.
Requires the write scope, modify on the board, and the page-layout API write flag on the
workspace (otherwise 403 page_layout_api_disabled).
modify is checked on the board that governs the design, which is usually the board in
the path but not always: one design document can be bound to more than one published page,
and the oldest binding governs all of them — an edit rewrites every page it serves. A caller
who may modify the board it addressed but not the governing one gets
403 board_not_authorized. A read-only board gets 403 board_read_only, and so does a
read-only subboard whose section you address with subboard_token.
Parameters whose value is a string — slot_id, element_id, subboard_token and the
other id arguments — may be sent either as query parameters or as keys of the JSON request
body. They are listed here as query parameters because this document carries one body
schema per operation; both forms reach the same handler.
Everything else must be a JSON body value. at_index, column_index,
create_new_column, element, ordered_ids and operations are typed (integer, boolean,
object, array) and a query-string value is always a string, so this API answers
?at_index=0 with 422 invalid_parameter rather than guessing what you meant. That is
deliberate: coercing here would make this endpoint accept a payload the batch endpoint
rejects.
DELETE is a further restriction: send its addressing in the query string. Request bodies
on DELETE are dropped by some proxies and cannot be sent at all by some clients, so a
body-addressed delete is a delete that can silently lose its target.
Freshness. This API is immediately current: a follow-up GET .../page_layout reflects
this write straight away. The public page at /s/<slug> is only eventually current — the
write purges the cached render, but edge points of presence can keep serving a recent copy
for up to an hour. Do not tell a user to reload and expect to see the change.
Concurrency. You are not the only writer: a person can be editing the same page in the
Publish editor while you write. Send the version_token from GET .../page_layout back as
expected_version_token and this call becomes a compare-and-swap — if the document moved in
between you get 409 stale_document and nothing is applied, so read again, redecide and
re-send rather than overwriting whoever moved it. Omit it and the write simply applies. It
matters most on PATCH, which replaces an element wholesale.
Did it actually do anything? A successful write can legitimately change nothing — that is
what makes retries safe. converged: true means the goal state already held (a delete of an
id that is not there, a move that would change no order, a re-sent identical add).
materialized: true means the page had no stored document and this call saved the template
design it was already rendering; it can arrive together with converged. Both keys are
absent when false, never false.
Absent is not the same as false. converged is a claim four of the five verbs make and
PATCH makes none: its merge is unconditional, so it never reports convergence even when it
replaced an element with byte-identical content. Absent therefore means "this verb has no
opinion", not "something changed". On PATCH — the verb the concurrency note above singles
out — compare the version_token you sent with the one you got back if you need to know
whether the row moved, and read the element back if you need to know whether the page did.
Request
Responses
- 200
- 404
- 409
the whole sequence applied
one operation failed, so nothing was applied
Conflict: the request was well formed and disagrees with the current state. FOUR codes share
this status. Key on extensions.code, never on the status and never on retryable alone:
stale_document— the stored document changed since theversion_tokenyou echoed asexpected_version_token, so nothing was applied. Read the page again, re-decide, and re-send.retryable: false, because an identical re-send carries the same now-stale token.element_exists— the element id you supplied is already live carrying different content: two logical adds colliding on one id. Choose another id, orPATCHthe element that is there.retryable: false.lock_wait_timeout— another writer held this page's row for too long. This is the one code here where re-sending the identical call is correct.retryable: true.page_layout_library_locked— this workspace's published-links library is being set up by another request. Nothing was changed.retryable: true.