Skip to content

REST API Reference

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:

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.

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 for backups and the MCP byline tools for supported byline management. OAuth endpoints are protocol endpoints; discover them from the metadata described in the MCP OAuth section instead of treating them as application REST endpoints.

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:

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 and token 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.

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

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.

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

{
"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:

{
"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.

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.

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
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
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
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
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
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
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
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
Method Path Operation Summary
GET /_emdash/api/settings getSettings Get site settings
PUT /_emdash/api/settings updateSettings Update site settings
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
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
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
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

The content lifecycle reference 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 the entry before changing it:

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

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

{
"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:

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, 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.

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:

{
"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.

A reference field 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:

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() and getEmDashReferences().

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.

Media operations cover listing, upload, metadata updates, image replacement, folders, usage information, and usage-index maintenance. The media library guide explains the user-facing workflow and the meaning of usage coverage.

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 and gallery 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.

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

Terminal window
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.

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

    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

    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.

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 before starting another upload.

Folder names are trimmed, limited to 200 characters, and compared after Unicode normalization and lowercasing. Names such as Photos, photos, and PHOTOS 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.

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.

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 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.

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.

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.

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.