REST API Reference

On this page

EmDash exposes a REST API at /_emdash/api/ for content management, media uploads, and schema operations.

Authentication

API requests require authentication via a Bearer token in the Authorization header:

Authorization: Bearer <token>

Generate tokens through the admin interface or programmatically.

Response format

All responses follow a consistent format. A successful response wraps the result in data:

{
  "success": true,
  "data": { ... }
}

An error response includes a code, message, and optional details:

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message",
    "details": { ... }
  }
}

Content Endpoints

List Content

GET /_emdash/api/content/:collection

Parameters

ParameterTypeDescription
collectionstringCollection slug (path)
cursorstringPagination cursor (query)
limitnumberItems per page (query, default: 50)
statusstringFilter by status (query)
orderBystringField to sort by (query)
orderstringSort direction: asc or desc (query)

Response

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "01HXK5MZSN...",
        "type": "posts",
        "slug": "hello-world",
        "data": { "title": "Hello World", ... },
        "status": "published",
        "createdAt": "2025-01-24T12:00:00Z",
        "updatedAt": "2025-01-24T12:00:00Z"
      }
    ],
    "nextCursor": "eyJpZCI6..."
  }
}

Get Content

GET /_emdash/api/content/:collection/:id

Response

{
  "success": true,
  "data": {
    "item": {
      "id": "01HXK5MZSN...",
      "type": "posts",
      "slug": "hello-world",
      "data": { "title": "Hello World", ... },
      "status": "published",
      "createdAt": "2025-01-24T12:00:00Z",
      "updatedAt": "2025-01-24T12:00:00Z"
    }
  }
}

Create Content

POST /_emdash/api/content/:collection
Content-Type: application/json

Request Body

{
  "data": {
    "title": "New Post",
    "content": [...]
  },
  "slug": "new-post",
  "status": "draft"
}

Response

{
  "success": true,
  "data": {
    "item": { ... }
  }
}

Update Content

PUT /_emdash/api/content/:collection/:id
Content-Type: application/json

Request Body

{
	"data": {
		"title": "Updated Title"
	},
	"status": "published"
}

Delete Content

DELETE /_emdash/api/content/:collection/:id

Response

{
	"success": true,
	"data": {
		"success": true
	}
}

Media Endpoints

List Media

GET /_emdash/api/media?includeUsage=1

Parameters

ParameterTypeDescription
cursorstringOpaque pagination cursor
limitnumberItems per page, from 1 to 100 (default: 50)
mimeTypestringFilter by one or more comma-separated MIME types
qstringCase-insensitive filename search
includeUsage1Include a coverage-aware usage summary on every returned item

Response

{
	"data": {
		"items": [
			{
				"id": "01HXK5MZSN...",
				"filename": "photo.jpg",
				"mimeType": "image/jpeg",
				"size": 102400,
				"width": 1920,
				"height": 1080,
				"url": "/_emdash/api/media/file/uploads/photo.jpg",
				"createdAt": "2025-01-24T12:00:00Z",
				"usage": {
					"count": 3,
					"coverage": {
						"scope": "all_content_collections",
						"status": "complete"
					}
				}
			}
		],
		"nextCursor": "eyJpZCI6..."
	}
}

Get Media

GET /_emdash/api/media/:id?includeUsage=1

includeUsage is optional on both list and get. Its only accepted value is 1. When omitted, the usage property is omitted and the server does not run usage queries.

Usage Summaries

usage.count is the number of distinct active EmDash content rows or locales whose selected current indexed source references the media item. Repeated references and multiple source variants for the same content entry count once. Trashed content does not count.

A numeric count can reveal draft-like content. It is returned only when a session user has content:read_drafts, or when an API token has admin scope and its associated user also has that permission. Other media readers receive usage.count: null; this is a successful redacted response, not an error.

Every requested summary includes aggregate coverage for all currently registered content collections:

StatusMeaning
completeEvery registered collection has current, completed usage coverage
neverNo registered collection has completed an initial usage repair
runningA usage repair is currently running
partialCoverage is mixed or only part of the registered scope was indexed
failedCoverage failed across the registered scope
staleIndexed coverage is outdated
unknownStored coverage contains a state this version does not recognize

Only complete supports a scoped complete-zero statement within the EmDash-managed fields described below. Counts with any other status are indexed projections and may over-report or under-report. Even complete results are advisory during concurrent writes; usage reads are not a transactional lock and must not be used as a deletion guarantee.

Get Media Usage Details

GET /_emdash/api/media/:id/usage?limit=50&cursor=...

This endpoint requires media:read and content:read_drafts. Token-authenticated callers also require admin scope; token scope does not bypass the associated user’s permissions.

limit controls content entry groups per page, from 1 to 100 (default: 50). Pagination never splits the sources or occurrences for one returned entry group.

{
	"data": {
		"items": [
			{
				"collection": "posts",
				"contentId": "01CONTENT...",
				"title": "Launch notes",
				"slug": "launch-notes",
				"locale": "en",
				"status": "published",
				"scheduledAt": null,
				"deletedAt": null,
				"sources": [
					{
						"variant": "columns",
						"occurrences": [
							{
								"fieldSlug": "hero",
								"fieldPath": "hero",
								"occurrenceIndex": 0,
								"referenceType": "image_field"
							}
						]
					}
				]
			}
		],
		"nextCursor": "eyJvcmRlclZhbHVlIjoicG9zdHMiLCJpZCI6IjAxLi4uIn0",
		"coverage": {
			"scope": "all_content_collections",
			"status": "complete"
		}
	}
}

Authorized details include active and trashed entries. A non-null deletedAt identifies a trashed entry. Sources are columns or draft_overlay; occurrences identify the supported field and path without exposing internal index metadata.

Media usage covers local media references in top-level image and file fields, repeater image fields, and Portable Text image blocks managed by EmDash content collections. It does not scan custom code, rendered HTML, settings, menus, widgets, plugin-private data, external sites, or provider-only assets.

Create Media

POST /_emdash/api/media
Content-Type: application/json

Request Body

{
	"filename": "photo.jpg",
	"mimeType": "image/jpeg",
	"size": 102400,
	"width": 1920,
	"height": 1080,
	"storageKey": "uploads/photo.jpg"
}

Update Media

PUT /_emdash/api/media/:id
Content-Type: application/json

Request Body

{
	"alt": "Photo description",
	"caption": "Photo caption"
}

Delete Media

DELETE /_emdash/api/media/:id

Repair Media Usage

POST /_emdash/api/admin/media-usage/repair
Content-Type: application/json
X-EmDash-Request: 1

Repairs the content media usage index for one collection or for all content collections. This is an admin/operator endpoint: session-authenticated callers need schema:manage, and bearer tokens must have the admin scope because the route is under /_emdash/api/admin.

All-content repair runs synchronously and sequentially in the current version. It can be expensive on large sites, so callers should trigger it deliberately and wait for the response.

Request Body

Repair one collection:

{
	"scope": "collection",
	"collection": "posts"
}

Repair all content collections:

{
	"scope": "all"
}

The request body is required. Invalid slugs, unknown request keys, missing scope, and body-less requests return 400 instead of defaulting to all-content repair.

Response

The endpoint returns 200 when a repair invocation produces a structured result. Inspect data.status: failed and stale are repair-domain statuses, not transport errors.

{
	"data": {
		"status": "complete",
		"indexedSourceCount": 12,
		"failedSourceCount": 0,
		"skippedSourceCount": 0,
		"deletedSourceCount": 1,
		"collections": [
			{
				"collection": "posts",
				"status": "complete",
				"indexedSourceCount": 12,
				"failedSourceCount": 0,
				"skippedSourceCount": 0,
				"deletedSourceCount": 1,
				"lastErrorCode": null,
				"startedAt": "2026-07-07T12:00:00.000Z",
				"completedAt": "2026-07-07T12:00:01.000Z"
			}
		]
	}
}

Top-level response fields:

FieldTypeDescription
statuscomplete | partial | failed | staleAggregate repair status
indexedSourceCountnumberSources indexed during repair
failedSourceCountnumberSources that failed during repair
skippedSourceCountnumberSources skipped, including stale conflicts
deletedSourceCountnumberStale usage rows deleted during repair
collectionsarrayPer-collection repair summaries

Collection summary fields:

FieldTypeDescription
collectionstringCollection slug
statuscomplete | partial | failed | staleCollection repair status
indexedSourceCountnumberSources indexed for this collection
failedSourceCountnumberSources that failed for this collection
skippedSourceCountnumberSources skipped for this collection
deletedSourceCountnumberStale usage rows deleted for this collection
lastErrorCodestring | nullLast collection repair error, when available
startedAtstringRepair start time
completedAtstring | nullCompletion time, or null for stale results

Unknown collections return 200 with data.status: "failed" and a per-collection lastErrorCode such as COLLECTION_NOT_FOUND. Transport errors still use the standard error envelope, including 400, 401, 403, 413, and 500.

Get Media File

GET /_emdash/api/media/file/:key

Serves the actual file content. For local storage only.

Revision Endpoints

List Revisions

GET /_emdash/api/content/:collection/:entryId/revisions

Parameters

ParameterTypeDescription
limitnumberMax revisions to return (default: 50)

Response

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "01HXK5MZSN...",
        "collection": "posts",
        "entryId": "01HXK5MZSN...",
        "data": { ... },
        "createdAt": "2025-01-24T12:00:00Z"
      }
    ],
    "total": 5
  }
}

Get Revision

GET /_emdash/api/revisions/:revisionId

Restore Revision

POST /_emdash/api/revisions/:revisionId/restore

Restores content to this revision’s state and creates a new revision.

Schema Endpoints

List Collections

GET /_emdash/api/schema/collections

Response

{
	"success": true,
	"data": {
		"items": [
			{
				"id": "01HXK5MZSN...",
				"slug": "posts",
				"label": "Posts",
				"labelSingular": "Post",
				"supports": ["drafts", "revisions", "preview"]
			}
		]
	}
}

Get Collection

GET /_emdash/api/schema/collections/:slug

Parameters

ParameterTypeDescription
includeFieldsbooleanInclude field definitions (query)

Create Collection

POST /_emdash/api/schema/collections
Content-Type: application/json

Request Body

{
	"slug": "products",
	"label": "Products",
	"labelSingular": "Product",
	"description": "Product catalog",
	"supports": ["drafts", "revisions"]
}

Update Collection

PUT /_emdash/api/schema/collections/:slug
Content-Type: application/json

Delete Collection

DELETE /_emdash/api/schema/collections/:slug

Parameters

ParameterTypeDescription
forcebooleanDelete even if collection has content (query)

List Fields

GET /_emdash/api/schema/collections/:slug/fields

Create Field

POST /_emdash/api/schema/collections/:slug/fields
Content-Type: application/json

Request Body

{
	"slug": "price",
	"label": "Price",
	"type": "number",
	"required": true,
	"validation": {
		"min": 0
	}
}

Update Field

PUT /_emdash/api/schema/collections/:collectionSlug/fields/:fieldSlug
Content-Type: application/json

Delete Field

DELETE /_emdash/api/schema/collections/:collectionSlug/fields/:fieldSlug

Reorder Fields

POST /_emdash/api/schema/collections/:slug/fields/reorder
Content-Type: application/json

Request Body

{
	"fieldSlugs": ["title", "content", "author", "publishedAt"]
}

Schema Export

Export Schema (JSON)

GET /_emdash/api/schema
Accept: application/json

Export Schema (TypeScript)

GET /_emdash/api/schema?format=typescript
Accept: text/typescript

Returns TypeScript interfaces for all collections.

Plugin Endpoints

List Plugins

GET /_emdash/api/admin/plugins

Get Plugin

GET /_emdash/api/admin/plugins/:id

Enable Plugin

POST /_emdash/api/admin/plugins/:id/enable

Disable Plugin

POST /_emdash/api/admin/plugins/:id/disable

Error Codes

CodeHTTP StatusDescription
NOT_FOUND404Resource not found
VALIDATION_ERROR400Invalid input data
UNAUTHORIZED401Missing or invalid token
FORBIDDEN403Insufficient permissions
CONTENT_LIST_ERROR500Failed to list content
CONTENT_CREATE_ERROR500Failed to create content
CONTENT_UPDATE_ERROR500Failed to update content
CONTENT_DELETE_ERROR500Failed to delete content
MEDIA_LIST_ERROR500Failed to list media
MEDIA_CREATE_ERROR500Failed to create media
SCHEMA_CREATE_ERROR500Schema operation failed
SLUG_CONFLICT409Slug already exists
RESERVED_SLUG400Slug is reserved

Search Endpoints

GET /_emdash/api/search?q=hello+world

Parameters

ParameterTypeDescription
qstringSearch query (required)
collectionsstringComma-separated collection slugs
statusstringFilter by status (default: published)
limitnumberMax results (default: 20)
cursorstringPagination cursor

Response

{
  "success": true,
  "data": {
    "items": [
      {
        "collection": "posts",
        "id": "01HXK5MZSN...",
        "slug": "hello-world",
        "locale": "en",
        "title": "Hello World",
        "snippet": "...this is a <mark>hello</mark> <mark>world</mark> example...",
        "score": 0.95
      }
    ],
    "nextCursor": "eyJvZmZzZXQiOjIwfQ"
  }
}

Search Suggestions

GET /_emdash/api/search/suggest?q=hel&limit=5

Returns prefix-matched titles for autocomplete.

Rebuild Search Index

POST /_emdash/api/search/rebuild

Rebuild FTS index for all or specific collections.

Search Stats

GET /_emdash/api/search/stats

Returns indexed document counts per collection.

Section Endpoints

List Sections

GET /_emdash/api/sections
GET /_emdash/api/sections?source=theme
GET /_emdash/api/sections?search=newsletter

Get Section

GET /_emdash/api/sections/:slug

Create Section

POST /_emdash/api/sections
Content-Type: application/json

{
  "slug": "my-section",
  "title": "My Section",
  "keywords": ["keyword1"],
  "content": [...]
}

Update Section

PUT /_emdash/api/sections/:slug

Delete Section

DELETE /_emdash/api/sections/:slug

Settings Endpoints

Get All Settings

GET /_emdash/api/settings

Update Settings

POST /_emdash/api/settings
Content-Type: application/json

{
  "siteTitle": "My Site",
  "tagline": "A great site",
  "postsPerPage": 10
}

List Menus

GET /_emdash/api/menus

Get Menu

GET /_emdash/api/menus/:name

Create Menu

POST /_emdash/api/menus
Content-Type: application/json

{
  "name": "footer",
  "label": "Footer Navigation"
}

Update Menu

PUT /_emdash/api/menus/:name

Delete Menu

DELETE /_emdash/api/menus/:name

Add Menu Item

POST /_emdash/api/menus/:name/items
Content-Type: application/json

{
  "type": "page",
  "referenceCollection": "pages",
  "referenceId": "page_about",
  "label": "About Us"
}

Reorder Menu Items

POST /_emdash/api/menus/:name/reorder
Content-Type: application/json

{
  "items": [
    { "id": "item_1", "parentId": null, "sortOrder": 0 },
    { "id": "item_2", "parentId": null, "sortOrder": 1 },
    { "id": "item_3", "parentId": "item_2", "sortOrder": 0 }
  ]
}

Taxonomy Endpoints

List Taxonomy Definitions

GET /_emdash/api/taxonomies

Create Taxonomy

POST /_emdash/api/taxonomies
Content-Type: application/json

{
  "name": "genre",
  "label": "Genres",
  "labelSingular": "Genre",
  "hierarchical": true,
  "collections": ["books", "movies"]
}

List Terms

GET /_emdash/api/taxonomies/:name/terms

Create Term

POST /_emdash/api/taxonomies/:name/terms
Content-Type: application/json

{
  "slug": "tutorials",
  "label": "Tutorials",
  "parentId": "term_abc",
  "description": "How-to guides"
}

Update Term

PUT /_emdash/api/taxonomies/:name/terms/:slug

Delete Term

DELETE /_emdash/api/taxonomies/:name/terms/:slug

Set Entry Terms

POST /_emdash/api/content/:collection/:id/terms/:taxonomy
Content-Type: application/json

{
  "termIds": ["term_news", "term_featured"]
}

Widget Area Endpoints

List Widget Areas

GET /_emdash/api/widget-areas

Get Widget Area

GET /_emdash/api/widget-areas/:name

Create Widget Area

POST /_emdash/api/widget-areas
Content-Type: application/json

{
  "name": "sidebar",
  "label": "Main Sidebar",
  "description": "Appears on posts"
}

Delete Widget Area

DELETE /_emdash/api/widget-areas/:name

Add Widget

POST /_emdash/api/widget-areas/:name/widgets
Content-Type: application/json

{
  "type": "content",
  "title": "About",
  "content": [...]
}

Update Widget

PUT /_emdash/api/widget-areas/:name/widgets/:id

Delete Widget

DELETE /_emdash/api/widget-areas/:name/widgets/:id

Reorder Widgets

POST /_emdash/api/widget-areas/:name/reorder
Content-Type: application/json

{
  "widgetIds": ["widget_1", "widget_2", "widget_3"]
}

User Management Endpoints

List Users

GET /_emdash/api/admin/users
GET /_emdash/api/admin/users?role=40
GET /_emdash/api/admin/users?search=john

Get User

GET /_emdash/api/admin/users/:id

Update User

PUT /_emdash/api/admin/users/:id
Content-Type: application/json

{
  "name": "John Doe",
  "role": 40
}

Enable User

POST /_emdash/api/admin/users/:id/enable

Disable User

POST /_emdash/api/admin/users/:id/disable

Authentication Endpoints

Setup Status

GET /_emdash/api/setup/status

Returns whether setup is complete and if users exist.

Passkey Login

POST /_emdash/api/auth/passkey/options

Get WebAuthn authentication options.

POST /_emdash/api/auth/passkey/verify
Content-Type: application/json

{
  "id": "credential-id",
  "rawId": "...",
  "response": {...},
  "type": "public-key"
}

Verify passkey and create session.

POST /_emdash/api/auth/magic-link/send
Content-Type: application/json

{
  "email": "[email protected]"
}
GET /_emdash/api/auth/magic-link/verify?token=xxx

Logout

POST /_emdash/api/auth/logout

Current User

GET /_emdash/api/auth/me

Invite User

POST /_emdash/api/auth/invite
Content-Type: application/json

{
  "email": "[email protected]",
  "role": 30
}

Passkey Management

GET /_emdash/api/auth/passkey

List user’s passkeys.

POST /_emdash/api/auth/passkey/register/options
POST /_emdash/api/auth/passkey/register/verify

Register new passkey.

PATCH /_emdash/api/auth/passkey/:id
Content-Type: application/json

{
  "name": "MacBook Pro"
}

Rename passkey.

DELETE /_emdash/api/auth/passkey/:id

Delete passkey.

Import Endpoints

Analyze WordPress Export

POST /_emdash/api/import/wordpress/analyze
Content-Type: multipart/form-data

file: <WXR file>

Execute WordPress Import

POST /_emdash/api/import/wordpress/execute
Content-Type: application/json

{
  "analysisId": "...",
  "options": {
    "includeMedia": true,
    "includeTaxonomies": true,
    "includeMenus": true
  }
}

Rate Limiting

API endpoints may be rate-limited based on deployment configuration. When rate-limited, responses include:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

CORS

The API supports CORS for browser requests. Configure allowed origins in your deployment.