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
| Parameter | Type | Description |
|---|---|---|
collection | string | Collection slug (path) |
cursor | string | Pagination cursor (query) |
limit | number | Items per page (query, default: 50) |
status | string | Filter by status (query) |
orderBy | string | Field to sort by (query) |
order | string | Sort 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
| Parameter | Type | Description |
|---|---|---|
cursor | string | Opaque pagination cursor |
limit | number | Items per page, from 1 to 100 (default: 50) |
mimeType | string | Filter by one or more comma-separated MIME types |
q | string | Case-insensitive filename search |
includeUsage | 1 | Include 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:
| Status | Meaning |
|---|---|
complete | Every registered collection has current, completed usage coverage |
never | No registered collection has completed an initial usage repair |
running | A usage repair is currently running |
partial | Coverage is mixed or only part of the registered scope was indexed |
failed | Coverage failed across the registered scope |
stale | Indexed coverage is outdated |
unknown | Stored 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:
| Field | Type | Description |
|---|---|---|
status | complete | partial | failed | stale | Aggregate repair status |
indexedSourceCount | number | Sources indexed during repair |
failedSourceCount | number | Sources that failed during repair |
skippedSourceCount | number | Sources skipped, including stale conflicts |
deletedSourceCount | number | Stale usage rows deleted during repair |
collections | array | Per-collection repair summaries |
Collection summary fields:
| Field | Type | Description |
|---|---|---|
collection | string | Collection slug |
status | complete | partial | failed | stale | Collection repair status |
indexedSourceCount | number | Sources indexed for this collection |
failedSourceCount | number | Sources that failed for this collection |
skippedSourceCount | number | Sources skipped for this collection |
deletedSourceCount | number | Stale usage rows deleted for this collection |
lastErrorCode | string | null | Last collection repair error, when available |
startedAt | string | Repair start time |
completedAt | string | null | Completion 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
| Parameter | Type | Description |
|---|---|---|
limit | number | Max 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
| Parameter | Type | Description |
|---|---|---|
includeFields | boolean | Include 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
| Parameter | Type | Description |
|---|---|---|
force | boolean | Delete 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
| Code | HTTP Status | Description |
|---|---|---|
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 400 | Invalid input data |
UNAUTHORIZED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Insufficient permissions |
CONTENT_LIST_ERROR | 500 | Failed to list content |
CONTENT_CREATE_ERROR | 500 | Failed to create content |
CONTENT_UPDATE_ERROR | 500 | Failed to update content |
CONTENT_DELETE_ERROR | 500 | Failed to delete content |
MEDIA_LIST_ERROR | 500 | Failed to list media |
MEDIA_CREATE_ERROR | 500 | Failed to create media |
SCHEMA_CREATE_ERROR | 500 | Schema operation failed |
SLUG_CONFLICT | 409 | Slug already exists |
RESERVED_SLUG | 400 | Slug is reserved |
Search Endpoints
Global Search
GET /_emdash/api/search?q=hello+world
Parameters
| Parameter | Type | Description |
|---|---|---|
q | string | Search query (required) |
collections | string | Comma-separated collection slugs |
status | string | Filter by status (default: published) |
limit | number | Max results (default: 20) |
cursor | string | Pagination 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
}
Menu Endpoints
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.
Magic Link
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.