# REST API Reference

> The public HTTP contract, authentication rules, and complete OpenAPI operation inventory.

Source: https://emdashcmseverything.com/official-docs/reference/rest-api/

EmDash exposes its supported application programming interface (API) under `/_emdash/api/`. Use the generated OpenAPI 3.1 document for request parameters, bodies, response schemas, status codes, and client generation:

```http
GET /_emdash/api/openapi.json
```

The document is generated from the same Zod schemas used by the API. It also reflects the configured maximum media upload size.

## Public contract boundary

The OpenAPI document is the boundary of the supported REST API. EmDash also has routes for its admin interface and protocol workflows. A route that exists in the source tree but is absent from OpenAPI is not a supported REST operation for external clients.

This distinction applies to backup, byline administration, relation traversal, plugin management, setup, import, and authentication routes. Use the [backup guide](https://emdashcmseverything.com/official-docs/guides/backups/) for backups and the [MCP byline tools](https://emdashcmseverything.com/official-docs/reference/mcp-server/#byline-tools) for supported byline management. OAuth endpoints are protocol endpoints; discover them from the metadata described in the [MCP OAuth section](https://emdashcmseverything.com/official-docs/reference/mcp-server/#oauth-discovery) instead of treating them as application REST endpoints.

## Authentication and authorization

Most operations accept either an EmDash session cookie or a Bearer token. Send a personal access token or OAuth access token in the `Authorization` header:

```http
Authorization: Bearer $EMDASH_TOKEN
```

Bearer tokens are limited by their scopes and the associated user's role. Session requests use the user's role. See [user roles](https://emdashcmseverything.com/official-docs/guides/authentication/#user-roles) and [token scopes](https://emdashcmseverything.com/official-docs/reference/mcp-server/#scopes) for the authorization model.

`GET` and `POST /_emdash/api/comments/{collection}/{contentId}` are public. The `GET` operation returns approved comments; the `POST` operation submits a comment for moderation. The remaining comment-moderation operations require authentication.

### Cross-site request forgery protection

For a state-changing request authenticated by a session cookie, include this header:

```http
X-EmDash-Request: 1
```

Bearer-token requests do not require the header because they do not use ambient browser credentials. Browser requests to a public write operation must either send the header or have an `Origin` that matches the EmDash site's public or request origin.

## Response envelopes

A successful JSON response sets `success` to `true` and places the operation-specific result in `data`:

```json
{
	"success": true,
	"data": {
		"items": []
	}
}
```

An error sets `success` to `false` and includes a stable machine-readable code and a message. Some errors also include structured `details`:

```json
{
	"success": false,
	"error": {
		"code": "NOT_FOUND",
		"message": "Content item not found"
	}
}
```

Use the status codes and error schemas on each OpenAPI operation. Common statuses are `400` for invalid input, `401` for missing or invalid credentials, `403` for insufficient scope or permission, `404` for a missing resource, `409` for a state conflict, `413` for an oversized upload, `422` for a save rejected by a plugin, and `500` for an internal failure.

## Endpoint inventory

The operation ID is stable within the generated contract and is commonly used as the method name by OpenAPI client generators. The following inventory is checked against the generated OpenAPI document.

{/* rest-endpoint-inventory:start */}

### Content

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/content/{collection}` | `listContent` | List content items |
| `POST` | `/_emdash/api/content/{collection}` | `createContent` | Create a content item |
| `GET` | `/_emdash/api/content/{collection}/{id}` | `getContent` | Get a content item |
| `PUT` | `/_emdash/api/content/{collection}/{id}` | `updateContent` | Update a content item |
| `DELETE` | `/_emdash/api/content/{collection}/{id}` | `deleteContent` | Delete a content item (soft delete) |
| `POST` | `/_emdash/api/content/{collection}/{id}/publish` | `publishContent` | Publish a content item |
| `POST` | `/_emdash/api/content/{collection}/{id}/unpublish` | `unpublishContent` | Unpublish a content item |
| `POST` | `/_emdash/api/content/{collection}/{id}/schedule` | `scheduleContent` | Schedule content for future publishing |
| `DELETE` | `/_emdash/api/content/{collection}/{id}/schedule` | `unscheduleContent` | Cancel scheduled publishing |
| `POST` | `/_emdash/api/content/{collection}/{id}/duplicate` | `duplicateContent` | Duplicate a content item |
| `POST` | `/_emdash/api/content/{collection}/{id}/restore` | `restoreContent` | Restore a content item from trash |
| `DELETE` | `/_emdash/api/content/{collection}/{id}/permanent` | `permanentDeleteContent` | Permanently delete a content item |
| `GET` | `/_emdash/api/content/{collection}/{id}/compare` | `compareContent` | Compare live and draft revisions |
| `POST` | `/_emdash/api/content/{collection}/{id}/discard-draft` | `discardDraft` | Discard draft changes |
| `GET` | `/_emdash/api/content/{collection}/{id}/lock` | `getEntryLock` | Read the entry's edit lock |
| `POST` | `/_emdash/api/content/{collection}/{id}/lock` | `acquireEntryLock` | Take or refresh the entry's edit lock |
| `DELETE` | `/_emdash/api/content/{collection}/{id}/lock` | `releaseEntryLock` | Release the caller's edit lock |
| `GET` | `/_emdash/api/content/{collection}/{id}/translations` | `getContentTranslations` | Get translations for a content item |
| `GET` | `/_emdash/api/content/{collection}/{id}/terms/{taxonomy}` | `getContentTerms` | Get taxonomy terms assigned to a content item |
| `POST` | `/_emdash/api/content/{collection}/{id}/terms/{taxonomy}` | `setContentTerms` | Set taxonomy terms on a content item |
| `GET` | `/_emdash/api/content/{collection}/authors` | `listContentAuthors` | List distinct authors of a collection's content |
| `GET` | `/_emdash/api/content/{collection}/trash` | `listTrashedContent` | List trashed content items |

### Media

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/media` | `listMedia` | List media items |
| `POST` | `/_emdash/api/media` | `uploadMedia` | Upload a media item |
| `GET` | `/_emdash/api/media/folders` | `listMediaFolders` | List media folders |
| `POST` | `/_emdash/api/media/folders` | `createMediaFolder` | Create a media folder |
| `GET` | `/_emdash/api/media/folders/{id}` | `getMediaFolder` | Get a media folder |
| `PUT` | `/_emdash/api/media/folders/{id}` | `updateMediaFolder` | Update a media folder |
| `DELETE` | `/_emdash/api/media/folders/{id}` | `deleteMediaFolder` | Delete a media folder |
| `GET` | `/_emdash/api/media/{id}` | `getMedia` | Get a media item |
| `PUT` | `/_emdash/api/media/{id}` | `updateMedia` | Update media metadata |
| `DELETE` | `/_emdash/api/media/{id}` | `deleteMedia` | Delete a media item |
| `GET` | `/_emdash/api/media/{id}/usage` | `getMediaUsage` | Get media usage details |
| `PUT` | `/_emdash/api/media/{id}/replace` | `replaceMediaImage` | Replace a media image |
| `POST` | `/_emdash/api/admin/media-usage/repair` | `repairMediaUsage` | Repair media usage indexes |
| `GET` | `/_emdash/api/admin/media-usage/progress` | `getMediaUsageProgress` | Get media usage indexing progress |
| `POST` | `/_emdash/api/admin/media-usage/progress` | `advanceMediaUsageProgress` | Advance media usage indexing |
| `GET` | `/_emdash/api/admin/media-usage/work` | `listMediaUsageWork` | List durable media usage work |
| `GET` | `/_emdash/api/admin/media-usage/activation` | `getMediaUsageActivation` | Get media usage activation status |
| `POST` | `/_emdash/api/admin/media-usage/activation` | `advanceMediaUsageActivation` | Advance media usage activation |
| `POST` | `/_emdash/api/admin/media-usage/work/retry` | `retryMediaUsageWork` | Retry one durable media usage job |
| `GET` | `/_emdash/api/admin/media-usage/collection-deletions` | `listMediaUsageCollectionDeletions` | List durable collection deletions |
| `POST` | `/_emdash/api/admin/media-usage/collection-deletions/retry` | `retryMediaUsageCollectionDeletion` | Retry one collection deletion |
| `POST` | `/_emdash/api/media/upload-url` | `getMediaUploadUrl` | Get a media upload target |
| `POST` | `/_emdash/api/media/{id}/confirm` | `confirmMediaUpload` | Confirm a media upload |
| `PUT` | `/_emdash/api/media/{id}/upload` | `uploadPendingMedia` | Upload a pending media file through EmDash |

### Schema

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/schema/block-types` | `listBlockTypes` | List block types |
| `POST` | `/_emdash/api/schema/block-types` | `createBlockType` | Create a block type |
| `GET` | `/_emdash/api/schema/block-types/{slug}` | `getBlockType` | Get a block type |
| `PUT` | `/_emdash/api/schema/block-types/{slug}` | `updateBlockType` | Update a block type |
| `POST` | `/_emdash/api/schema/block-types/{slug}/versions/{version}/activate` | `activateBlockTypeVersion` | Activate a block type version |
| `GET` | `/_emdash/api/schema/collections` | `listCollections` | List all collections |
| `POST` | `/_emdash/api/schema/collections` | `createCollection` | Create a collection |
| `GET` | `/_emdash/api/schema/collections/{slug}` | `getCollection` | Get a collection |
| `PUT` | `/_emdash/api/schema/collections/{slug}` | `updateCollection` | Update a collection |
| `DELETE` | `/_emdash/api/schema/collections/{slug}` | `deleteCollection` | Delete a collection |
| `GET` | `/_emdash/api/schema/collections/{slug}/fields` | `listFields` | List fields for a collection |
| `POST` | `/_emdash/api/schema/collections/{slug}/fields` | `createField` | Create a field |
| `GET` | `/_emdash/api/schema/collections/{slug}/fields/{fieldSlug}` | `getField` | Get a field |
| `PUT` | `/_emdash/api/schema/collections/{slug}/fields/{fieldSlug}` | `updateField` | Update a field |
| `DELETE` | `/_emdash/api/schema/collections/{slug}/fields/{fieldSlug}` | `deleteField` | Delete a field |
| `POST` | `/_emdash/api/schema/collections/reorder` | `reorderCollections` | Reorder collections in the admin sidebar |
| `POST` | `/_emdash/api/schema/collections/{slug}/fields/reorder` | `reorderFields` | Reorder fields in a collection |
| `GET` | `/_emdash/api/schema/orphans` | `listOrphanedTables` | List orphaned content tables |
| `POST` | `/_emdash/api/schema/orphans/{slug}` | `registerOrphanedTable` | Register an orphaned table as a collection |

### Comments

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/comments/{collection}/{contentId}` | `listPublicComments` | List approved comments for content |
| `POST` | `/_emdash/api/comments/{collection}/{contentId}` | `createComment` | Submit a new comment |
| `GET` | `/_emdash/api/admin/comments` | `listAdminComments` | List comments for moderation |
| `GET` | `/_emdash/api/admin/comments/counts` | `getCommentCounts` | Get comment status counts |
| `POST` | `/_emdash/api/admin/comments/bulk` | `bulkCommentAction` | Bulk approve, spam, trash, or delete comments |
| `GET` | `/_emdash/api/admin/comments/{id}` | `getComment` | Get a single comment |
| `DELETE` | `/_emdash/api/admin/comments/{id}` | `deleteComment` | Permanently delete a comment |
| `PUT` | `/_emdash/api/admin/comments/{id}/status` | `updateCommentStatus` | Change comment status |

### Taxonomies

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/taxonomies` | `listTaxonomies` | List all taxonomy definitions |
| `GET` | `/_emdash/api/taxonomies/{name}` | `getTaxonomy` | Get a taxonomy definition |
| `PUT` | `/_emdash/api/taxonomies/{name}` | `updateTaxonomy` | Update a taxonomy definition |
| `DELETE` | `/_emdash/api/taxonomies/{name}` | `deleteTaxonomy` | Delete a taxonomy, its terms, and their content assignments |
| `GET` | `/_emdash/api/taxonomies/{name}/translations` | `listTaxonomyTranslations` | List every locale variant of a taxonomy definition |
| `POST` | `/_emdash/api/taxonomies/{name}/reorder` | `reorderTerms` | Set the manual order of one sibling group of terms |
| `GET` | `/_emdash/api/taxonomies/{name}/terms` | `listTerms` | List terms for a taxonomy |
| `POST` | `/_emdash/api/taxonomies/{name}/terms` | `createTerm` | Create a term |
| `GET` | `/_emdash/api/taxonomies/{name}/terms/{slug}` | `getTerm` | Get a term by slug |
| `PUT` | `/_emdash/api/taxonomies/{name}/terms/{slug}` | `updateTerm` | Update a term |
| `DELETE` | `/_emdash/api/taxonomies/{name}/terms/{slug}` | `deleteTerm` | Delete a term |

### Menus

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/menus` | `listMenus` | List all menus with item counts |
| `POST` | `/_emdash/api/menus` | `createMenu` | Create a menu |
| `GET` | `/_emdash/api/menus/{name}` | `getMenu` | Get a menu with all items |
| `PUT` | `/_emdash/api/menus/{name}` | `updateMenu` | Update a menu |
| `DELETE` | `/_emdash/api/menus/{name}` | `deleteMenu` | Delete a menu and its items |
| `POST` | `/_emdash/api/menus/{name}/items` | `createMenuItem` | Add an item to a menu |
| `PUT` | `/_emdash/api/menus/{name}/items/{id}` | `updateMenuItem` | Update a menu item |
| `DELETE` | `/_emdash/api/menus/{name}/items/{id}` | `deleteMenuItem` | Delete a menu item |
| `POST` | `/_emdash/api/menus/{name}/reorder` | `reorderMenuItems` | Batch reorder menu items |

### Sections

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/sections` | `listSections` | List sections |
| `POST` | `/_emdash/api/sections` | `createSection` | Create a section |
| `GET` | `/_emdash/api/sections/{slug}` | `getSection` | Get a section by slug |
| `PUT` | `/_emdash/api/sections/{slug}` | `updateSection` | Update a section |
| `DELETE` | `/_emdash/api/sections/{slug}` | `deleteSection` | Delete a section |

### Widgets

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/widget-areas` | `listWidgetAreas` | List all widget areas |
| `POST` | `/_emdash/api/widget-areas` | `createWidgetArea` | Create a widget area |
| `GET` | `/_emdash/api/widget-areas/{name}` | `getWidgetArea` | Get a widget area with widgets |
| `DELETE` | `/_emdash/api/widget-areas/{name}` | `deleteWidgetArea` | Delete a widget area and its widgets |
| `POST` | `/_emdash/api/widget-areas/{name}/widgets` | `createWidget` | Add a widget to an area |
| `PUT` | `/_emdash/api/widget-areas/{name}/widgets/{id}` | `updateWidget` | Update a widget |
| `DELETE` | `/_emdash/api/widget-areas/{name}/widgets/{id}` | `deleteWidget` | Delete a widget |
| `POST` | `/_emdash/api/widget-areas/{name}/reorder` | `reorderWidgets` | Reorder widgets in an area |

### Settings

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/settings` | `getSettings` | Get site settings |
| `PUT` | `/_emdash/api/settings` | `updateSettings` | Update site settings |

### Search

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/search` | `search` | Full-text search across collections |
| `GET` | `/_emdash/api/search/suggest` | `searchSuggest` | Autocomplete search suggestions |
| `POST` | `/_emdash/api/search/rebuild` | `rebuildSearchIndex` | Rebuild the search index for a collection |
| `POST` | `/_emdash/api/search/enable` | `enableSearch` | Enable or disable search for a collection |
| `GET` | `/_emdash/api/search/stats` | `getSearchStats` | Get search index statistics |

### Redirects

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/redirects` | `listRedirects` | List redirects |
| `POST` | `/_emdash/api/redirects` | `createRedirect` | Create a redirect rule |
| `GET` | `/_emdash/api/redirects/{id}` | `getRedirect` | Get a redirect |
| `PUT` | `/_emdash/api/redirects/{id}` | `updateRedirect` | Update a redirect |
| `DELETE` | `/_emdash/api/redirects/{id}` | `deleteRedirect` | Delete a redirect |
| `GET` | `/_emdash/api/redirects/404s` | `listNotFoundEntries` | List 404 log entries |
| `POST` | `/_emdash/api/redirects/404s` | `pruneNotFoundLog` | Prune old 404 log entries |
| `DELETE` | `/_emdash/api/redirects/404s` | `clearNotFoundLog` | Clear all 404 log entries |
| `GET` | `/_emdash/api/redirects/404s/summary` | `getNotFoundSummary` | Get 404 summary grouped by path |

### Users

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/admin/users` | `listUsers` | List users |
| `GET` | `/_emdash/api/admin/users/{id}` | `getUser` | Get user details |
| `PUT` | `/_emdash/api/admin/users/{id}` | `updateUser` | Update a user |
| `POST` | `/_emdash/api/admin/users/{id}/disable` | `disableUser` | Disable a user account |
| `POST` | `/_emdash/api/admin/users/{id}/enable` | `enableUser` | Enable a user account |
| `GET` | `/_emdash/api/admin/allowed-domains` | `listAllowedDomains` | List allowed email domains |
| `POST` | `/_emdash/api/admin/allowed-domains` | `createAllowedDomain` | Add an allowed email domain |
| `PUT` | `/_emdash/api/admin/allowed-domains/{domain}` | `updateAllowedDomain` | Update an allowed domain |
| `DELETE` | `/_emdash/api/admin/allowed-domains/{domain}` | `deleteAllowedDomain` | Remove an allowed domain |

### Transfer

| Method | Path | Operation | Summary |
| --- | --- | --- | --- |
| `GET` | `/_emdash/api/admin/transfer/capabilities` | `getTransferCapabilities` | Get site transfer capabilities |
| `GET` | `/_emdash/api/admin/transfer/imports` | `listTransferImports` | List site imports |
| `POST` | `/_emdash/api/admin/transfer/imports` | `createTransferImport` | Create a site import |
| `GET` | `/_emdash/api/admin/transfer/imports/{id}` | `getTransferImport` | Get a site import |
| `GET` | `/_emdash/api/admin/transfer/imports/{id}/missing` | `listTransferImportMissingFiles` | List package files still to upload |
| `PUT` | `/_emdash/api/admin/transfer/imports/{id}/files/{path}` | `uploadTransferImportFile` | Upload one package file |
| `POST` | `/_emdash/api/admin/transfer/imports/{id}/analyze` | `analyzeTransferImport` | Advance import analysis |
| `GET` | `/_emdash/api/admin/transfer/imports/{id}/plan` | `getTransferImportPlan` | Get an import plan |
| `POST` | `/_emdash/api/admin/transfer/imports/{id}/cancel` | `cancelTransferImport` | Cancel a site import |
| `POST` | `/_emdash/api/admin/transfer/imports/{id}/abandon` | `abandonTransferImport` | Abandon a failed or cancelled import |
| `POST` | `/_emdash/api/admin/transfer/imports/{id}/execute` | `executeTransferImport` | Start a planned import |
| `POST` | `/_emdash/api/admin/transfer/imports/{id}/advance` | `advanceTransferImport` | Advance an executing import |
| `GET` | `/_emdash/api/admin/transfer/imports/{id}/receipt` | `getTransferImportReceipt` | Get an import receipt |
| `GET` | `/_emdash/api/admin/transfer/exports` | `listTransferExports` | List site exports |
| `POST` | `/_emdash/api/admin/transfer/exports` | `createTransferExport` | Start a site export |
| `GET` | `/_emdash/api/admin/transfer/exports/{id}` | `getTransferExport` | Get a site export |
| `POST` | `/_emdash/api/admin/transfer/exports/{id}/advance` | `advanceTransferExport` | Advance a site export |
| `GET` | `/_emdash/api/admin/transfer/exports/{id}/manifest` | `getTransferExportManifest` | Download an export manifest |
| `GET` | `/_emdash/api/admin/transfer/exports/{id}/files/{path}` | `downloadTransferExportFile` | Download one export file |
| `GET` | `/_emdash/api/admin/transfer/exports/{id}/archive` | `downloadTransferExportArchive` | Download an export archive |
| `GET` | `/_emdash/api/admin/transfer/approvals` | `listTransferApprovals` | List transfer approvals |
| `POST` | `/_emdash/api/admin/transfer/approvals/{id}/approve` | `approveTransferApproval` | Approve a transfer request |
| `POST` | `/_emdash/api/admin/transfer/approvals/{id}/deny` | `denyTransferApproval` | Deny a transfer request |

{/* rest-endpoint-inventory:end */}

## Content lifecycle and bylines

The [content lifecycle reference](https://emdashcmseverything.com/official-docs/reference/content-lifecycle/) defines the state, revision,
permission, conflict, and hook behavior shared by REST, MCP, the CLI, and the admin panel.

Content reads return an opaque `_rev` token when one is available. Send `_rev` with `PUT /content/{collection}/{id}` to prevent overwriting a change made since the read. A stale token produces a conflict; read the item again before retrying. The CLI makes this check mandatory for `content update`, while the REST field remains optional for clients that deliberately choose an unconditional write.

### Read and update an entry

Read the entry before changing it:

```http
GET /_emdash/api/content/articles/01JARTICLE0000000000000000
Authorization: Bearer $EMDASH_TOKEN
```

The response contains its fields, publication state, and revision token:

```json
{
	"success": true,
	"data": {
		"item": {
			"id": "01JARTICLE0000000000000000",
			"type": "articles",
			"slug": "launch-notes",
			"status": "published",
			"data": { "title": "Launch notes" }
		},
		"_rev": "opaque-revision-token"
	}
}
```

Send only the fields that need to change, together with the token from that read:

```http
PUT /_emdash/api/content/articles/01JARTICLE0000000000000000
Authorization: Bearer $EMDASH_TOKEN
Content-Type: application/json

{
	"data": { "title": "Updated launch notes" },
	"_rev": "opaque-revision-token"
}
```

Changing a published entry creates a draft while the previous version stays live. Call the compare operation to review both versions, then publish the draft or discard it. Unpublishing keeps the content and its publication date, cancels any pending schedule, and removes the entry from the live site.

Creation and update bodies accept byline credits, and content responses include the primary byline and ordered credits. The content list can filter by stored byline IDs and optionally include an author's inferred byline. Creating and managing the byline records themselves is available through the [MCP byline tools](https://emdashcmseverything.com/official-docs/reference/mcp-server/#byline-tools), not the public REST contract.

The lifecycle operations distinguish a soft delete from permanent deletion. Restore returns trashed content as a draft with no schedule; permanent deletion removes a trashed item and cannot be undone. Publish, unpublish, schedule, unschedule, compare, discard-draft, and duplicate are separate operations so clients can request one state transition at a time.

### Entry edit lock

Collections can take a seven-minute edit lock when an editor opens an entry. Use the three operations on `/content/{collection}/{id}/lock` to read, acquire or refresh, and release the lease.

A read or acquire response shows whether locking is enabled, whether the caller holds the lease, and who currently holds it:

```json
{
	"success": true,
	"data": {
		"enabled": true,
		"heldByCaller": false,
		"holder": {
			"userId": "01JUSER000000000000000000",
			"userName": "Ada",
			"acquiredAt": "2026-05-01T09:12:04.117Z",
			"expiresAt": "2026-05-01T09:19:04.117Z"
		}
	}
}
```

When a collection has edit locking disabled, `enabled` is `false` and no lease is taken. Acquiring the same lock again and saving the entry both extend a lease held by the caller.

The acquire body can include an opaque `token` that identifies one editing session and `takeover: true` when the user chooses to replace another editor's lease. Pass the same token as a query parameter when releasing the lock. A second tab from the same account then cannot release the first tab's lease accidentally.

When another user holds the lease, protected content writes return `409 ENTRY_LOCKED`. The error details identify the holder and expiry. To override the lock, send `"overrideLock": true` in the JSON body for a write that has a body, or `?overrideLock=true` for a `DELETE` operation that has no body.

## Reference selections

A [`reference` field](https://emdashcmseverything.com/official-docs/reference/field-types/#reference) links an entry to entries in another collection through a relation. Its value is not part of `data`, and it is keyed by translation group, so every translation of an entry shares one selection.

Creation and update bodies carry selections under `references`, keyed by field slug, each an array of at most 1000 entry IDs in display order. EmDash writes the selection in the same transaction as the entry. The following update replaces the entry's author:

```http
PUT /_emdash/api/content/articles/01JARTICLE0000000000000000
Authorization: Bearer $EMDASH_TOKEN
Content-Type: application/json

{
	"data": { "title": "Updated launch notes" },
	"references": { "author": ["01JAUTHOR00000000000000000"] },
	"_rev": "opaque-revision-token"
}
```

A field bound to the child end of its relation selects the entries pointing at the one being written, which carry no order of their own. Relation limits are enforced on both ends, so a selection that would give a linked entry more parents than the relation allows is rejected alongside one that links too many entries itself.

On a collection that keeps revisions, a selection changed on a published entry is staged in the draft with the entry's other pending edits. It becomes live when the entry is published and is discarded with the draft. Publishing re-checks the whole selection against the relation's limits.

A single-entry read returns `references` keyed by field slug. Each field holds the first page of its linked entries, 50 of them, with a `nextCursor` when the field holds more, and reports each entry's ID, slug, collection, display title, resolved locale, and translation group. A caller who may read drafts sees a staged selection where the draft carries one. The content list operation does not include references.

Relation definitions and link traversal are admin routes, absent from OpenAPI and outside the public contract. Write and read selections through the content operations above, and render them on a site with [`getEmDashEntry()`](https://emdashcmseverything.com/official-docs/reference/api/#getemdashentry) and [`getEmDashReferences()`](https://emdashcmseverything.com/official-docs/reference/api/#getemdashreferences).

## Translations

The public REST contract exposes content translations and taxonomy-definition translations. Content creation accepts `translationOf`, and taxonomy creation uses the same field to add a locale variant. The content-term operations return locale-aware assignments.

`GET /taxonomies/{name}` returns the site's default-locale definition when `locale` is omitted, falling back to the lowest locale code only when the default locale has no definition. An update behaves differently: when `locale` is omitted, it changes the definition with the lowest locale code. Pass `locale` so a translated-taxonomy update reaches the intended definition. If that locale has no definition, the update returns `NOT_FOUND` instead of falling back to another locale. The translations operation returns every definition in the shared group and supplies the IDs accepted by `translationOf`.

`label` and `labelSingular` belong to one locale's definition. `hierarchical` and `collections` belong to the taxonomy: every locale returns the same values, and an update that sends either changes it for every locale. Creating a definition for a name that already exists in another locale adds it to that taxonomy, whether or not the request sends `translationOf`. The new definition takes the taxonomy's `hierarchical` and `collections`, and a create that sends different values returns `VALIDATION_ERROR`.

Deleting a taxonomy removes every locale of its definition, all its terms, and all assignments of those terms to content. It does not delete the content entries themselves.

The term-reorder operation changes one sibling group. The `ids` array may contain only part of that group; listed terms exchange their existing positions and omitted terms stay where they are. For example, reordering `[A, B, C]` with `ids: ["C", "A"]` produces `[C, B, A]`. Reordering does not change parent relationships, and one term order applies across every locale in its translation group.

Menu, taxonomy-term, and byline translation routes are not in the public REST contract. Their supported translation listings are available through `menu_translations`, `taxonomy_term_translations`, and `byline_translations` on the [MCP server](https://emdashcmseverything.com/official-docs/reference/mcp-server/#tool-inventory).

## Media endpoints

Media operations cover listing, upload, metadata updates, image replacement, folders, usage information, and usage-index maintenance. The [media library guide](https://emdashcmseverything.com/official-docs/guides/media-library/) explains the user-facing workflow and the meaning of usage coverage.

### List and inspect media

`GET /media` supports cursor or numbered-page pagination, MIME-type and filename filters, folders, and optional usage summaries. Omit `folderId` to include every folder, or pass `folderId=unfiled` to return only the Main library. Set `includeUsage=1` on a list or single-item read to include usage information; any other value is invalid.

`usage.count` counts distinct active content rows or locales whose current indexed source references the media item, plus each site setting that selects it (`logo`, `favicon`, `seo.defaultOgImage`). Repeated references in one entry count once, and trashed entries do not count. The number is visible only to callers who may read drafts; other authorized media readers receive `count: null` because the count could reveal draft content.

`GET /media/{id}/usage` returns the referencing content entries in pages. Every page also includes `siteSettings`, the site settings that select the media item, for example `[{ "setting": "favicon" }]`. Site settings are read from the stored settings on each request and do not depend on usage indexing.

Every usage result includes a coverage status:

| Status | Meaning |
| --- | --- |
| `complete` | Every registered collection has current usage coverage. |
| `never` | No registered collection has completed an initial usage repair. |
| `running` | A repair is in progress. |
| `partial` | Only part of the registered collection set has current coverage. |
| `failed` | Coverage failed across the registered collection set. |
| `stale` | The index is older than the content it describes. |
| `unknown` | The stored state is not recognized by this EmDash version. |

Only `complete` supports treating a zero count as complete within the indexed field types. Counts are advisory during concurrent writes; they do not lock the media item or guarantee that deletion is safe. Usage indexing covers image and file fields, repeater image fields, Portable Text image, gallery, and video blocks, and media declared by retained block versions in EmDash collections. The site logo, favicon, and default social image settings are also reported. Usage does not include custom Portable Text blocks, application code, rendered HTML, other settings, menus, widgets, plugin data, external sites, or provider-only assets.

### Direct multipart upload

Send a file through EmDash by posting it as the `file` field of a multipart request:

```bash
curl --request POST \
	--header "Authorization: Bearer $EMDASH_TOKEN" \
	--form "file=@./cover.jpg;type=image/jpeg" \
	https://example.com/_emdash/api/media
```

`curl` adds the multipart boundary. Do not set the `Content-Type` header manually. The OpenAPI `MediaDirectUploadBody` schema lists the optional metadata fields and the response schemas distinguish a new upload from a deduplicated existing item.

The multipart body can also include `alt` text and a `caption` for the new media item, image `width` and `height`, a `fieldId` whose MIME-type allowlist should be applied, and a downscaled `thumbnail` used to create a low-quality placeholder. An omitted or empty `alt` or `caption` field is stored as `null`. A new file returns `201 Created` and is ready immediately. Identical bytes return the existing media item with `200 OK` and `deduplicated: true`; the `alt` and `caption` sent with the upload are not applied to it.

### Upload target flow

Use the upload-target flow when the client can upload directly to S3-compatible storage. The media item remains pending and does not appear in the standard library until the confirmation succeeds.

1. Request an upload target

	```http
	POST /_emdash/api/media/upload-url
	Authorization: Bearer $EMDASH_TOKEN
	Content-Type: application/json

	{
		"filename": "cover.jpg",
		"contentType": "image/jpeg",
		"size": 102400
	}
	```

	The response supplies `uploadUrl`, `method`, `headers`, `mediaId`, `storageKey`, and an expiry. When `contentHash` matches an existing file with the same MIME type and size, the response instead sets `existing: true`; use that media item and do not upload or confirm another copy.

2. Upload the bytes

	Use the returned method and headers. Resolve a root-relative URL against the EmDash site and include the Bearer token. Send only the returned upload headers to an absolute URL on another origin.

3. Confirm the upload

	```http
	POST /_emdash/api/media/01JMEDIA000000000000000000/confirm
	Authorization: Bearer $EMDASH_TOKEN
	Content-Type: application/json

	{
		"size": 102400,
		"width": 1920,
		"height": 1080
	}
	```

	Confirmation verifies the stored object and changes the item from `pending` to `ready`. Supplied size and dimensions must match the uploaded file.

Local storage and native R2 return a same-origin EmDash upload target. S3-compatible storage may return a signed external URL. Pending items stay out of the standard media list until confirmation succeeds.

> **Caution**
>
> Do not send the EmDash Bearer token to an upload URL on another origin. The signed URL already authorizes the storage request; adding the token discloses it to the storage host.

### Upload errors

The following errors require a different recovery action:

| Status | Code | Action |
| --- | --- | --- |
| `400` | `NO_FILE` | Add the `file` field to a multipart request, or send the missing upload body. |
| `400` | `INVALID_TYPE` | Use an allowed MIME type that matches the pending media item. |
| `400` | `VALIDATION_ERROR` | Correct the missing or invalid metadata, including values above the configured size limit. |
| `400` | `FILE_NOT_FOUND` | Upload the object to the returned target before confirming it. |
| `400` | `UPLOAD_SIZE_MISMATCH` | Restart the flow with the correct size; the declared, uploaded, and confirmed sizes must agree. |
| `400` or `409` | `INVALID_STATE` | Read the media item before retrying. It may no longer be pending, or another request may have changed it during confirmation. |
| `404` | `NOT_FOUND` | Use an existing pending media ID. |
| `413` | `PAYLOAD_TOO_LARGE` | Reduce the file size or increase [`maxUploadSize`](https://emdashcmseverything.com/official-docs/reference/configuration/#maxuploadsize) before starting another upload. |

### Media folders

Folder names are trimmed, limited to 200 characters, and compared after Unicode normalization and lowercasing. Names such as `Photos`, `photos`, and `ＰＨＯＴＯＳ` therefore conflict. Deleting a folder returns its media to the Main library; it does not delete media, change media IDs or URLs, or change usage records.

### Repair media usage

Media usage activation, progress, work queues, deletion cleanup, and repair are operator operations under `/_emdash/api/admin/media-usage/`. Session users need `schema:manage`; Bearer tokens also need the `admin` scope.

If tracking is off, pause direct database writers before activation. EmDash temporarily blocks content and schema writes sent through its APIs while setup runs, but it cannot stop another process that writes directly to the database.

1. Stop direct database writers and wait for their in-progress writes to finish.
2. Read the activation state. `expanded` means tracking is off, `activating` means EmDash is preparing collections, and `active` means new media-reference changes are tracked.
3. Send one activation request with `{ "writersDrained": true }`.
4. Send progress requests one at a time, waiting for each returned `nextRequestInMs`, until activation is `active`.
5. Resume direct database writes.
6. Continue progress requests until historical indexing is `ready` and `nextRequestInMs` is `null`.

If a write request times out or returns `409` or `500`, read the activation and progress state before retrying. Completed batches remain recorded. When `lastErrorCode` is set, keep direct writers stopped, resolve the reported problem, and send one confirmed retry. Activation cannot be cancelled or reset after it starts, so test this procedure against a staging copy and keep a current database backup.

The work-list operations expose failed or delayed entry indexing without returning content, media references, lease tokens, raw database errors, or an exact backlog count. Retrying one item is idempotent. `409 WORK_LEASE_ACTIVE` means a worker is still processing the item; the response includes `details.leaseExpiresAt`, so wait until that time and read the item again before retrying. `409 WORK_CHANGED` means another request changed the work item; read its current state rather than overwriting the newer work.

The repair operation accepts either `{ "scope": "collection", "collection": "articles" }` or `{ "scope": "all" }`. An all-collection repair runs synchronously and sequentially, so it can take a long time on a large site. A `200` response can still report `partial`, `failed`, or `stale`; inspect `data.status`, the per-collection status, and the source counts before treating the repair as complete.

## Site transfer

The transfer operations under `/_emdash/api/admin/transfer/` export a site as a site package and import one into an empty site. The [site transfer guide](https://emdashcmseverything.com/official-docs/guides/site-transfer/) describes the export, upload, analysis, execution, and receipt workflow.

Session users need the `transfer:export` or `transfer:import` permission, which only administrators hold. Bearer tokens need `admin` or the `transfer:export`, `transfer:analyze`, or `transfer:execute` scope named by each operation. The approval operations accept signed-in sessions only. They decide requests made by the MCP `site_export_start` and `site_import_start` tools; the REST export and execute operations do not take an approval.

While an import is executing, and after it fails or is cancelled until it is abandoned, most other write operations return `503 TRANSFER_IMPORT_IN_PROGRESS`. The guide lists the [operations that stay available](https://emdashcmseverything.com/official-docs/guides/site-transfer/#writes-are-blocked-during-an-import).

## Pagination

List operations describe their pagination parameters in OpenAPI. Most cursor-paginated operations accept an opaque `cursor` and a `limit` from 1 to 100, defaulting to 50. Return the previous response's `nextCursor` unchanged; do not inspect or construct it. Some specialized lists use different limits, so generated clients should follow each operation's schema.

`GET /media`, `GET /content/{collection}`, and `GET /content/{collection}/trash` also accept a 1-based `page` instead of `cursor`. A numbered page has no `nextCursor` and reports how many items match: `totalCount` for media, `total` for content. Sending both `page` and `cursor` returns a `400` validation error.

## Search tokenizers

The search-enable operation stores a tokenizer for each collection. Changing it on an enabled collection rebuilds that collection's index.

| Value | Use |
| --- | --- |
| `porter unicode61` | The default for English content that benefits from Porter stemming. |
| `unicode61` | Languages that use word separators but should not use English stemming. |
| `trigram` | Text without spaces, including Japanese, Chinese, Thai, Khmer, Lao, and Burmese, or collections that need substring matching. Queries shorter than three Unicode characters return no matches. |

Disabling search preserves the stored tokenizer for the next enable operation. The rebuild operation uses the collection's stored tokenizer and field weights.

## Comments and redirects

Public comment submissions enter the moderation queue. Admin comment operations list all statuses, return counts, update one status, perform bulk moderation, and permanently delete a comment. A `429` response means the submission rate limit was reached.

Redirect operations manage redirect rules and the recorded 404 log separately. Pruning removes entries selected by the request body, while `DELETE /redirects/404s` clears the entire log. Neither operation deletes redirect rules.

> **Caution**
>
> Schema deletion, permanent content deletion, comment deletion, redirect-log clearing, and several media maintenance operations can remove data. Resolve the exact target and keep a current backup before sending an irreversible request.
