# Delete Page Element

```
DELETE 
/v1/:slug/boards/:token/page_layout/elements/:element_id
```

Removes an element and returns the slot as it now stands.

**Addressing is query-only.** `slot_id` and `subboard_token` are query parameters and this operation takes no request body at all: bodies on `DELETE` are dropped by some proxies and cannot be sent by some clients, so a body-addressed delete is one that can silently lose its target.

Convergent: an id that is nowhere in the document means the goal state already holds, so the call succeeds with the slot unchanged. An id that **is** in the document but not in the slot you addressed is a 404 — you addressed the wrong node, and answering "done" while it stayed on the page would be worse than failing.

Deleting a `section` does not tidy up after itself elsewhere; emptied columns are yours to maintain through the element payload.

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[​](#request "Direct link to request")

## Responses[​](#responses "Direct link to Responses")

* 200
* 409

element deleted (or already absent)

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 the `version_token` you echoed as `expected_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, or `PATCH` the 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`.
