How to Use the EmDash CMS API: A Developer Guide

EmDash's REST API is what the admin panel itself is built on — every button click in `/_emdash/admin` ultimately calls the same endpoints documented here. That means the API is genuinely complete, not a stripped-down subset. This is a practical map of it for scripts, CI pipelines, and anything else calling EmDash from outside the admin UI.
Table of Contents
- Authentication
- Response Format
- Content Endpoints
- Media Endpoints
- Schema Endpoints
- Search Endpoints
- Revision Endpoints
- Plugin Endpoints
- Optimistic Concurrency: The _rev Token
- A Practical Alternative: The CLI
- CORS for Browser-Based Requests
- Frequently Asked Questions
- Is there a rate limit?
- Can I use this API from a completely separate application, not just scripts?
- Does creating content via the API auto-publish it?
- How do I find a collection's exact field names before writing a create request?
- The Bottom Line
Authentication
Authorization: Bearer <token>Generate a token from Admin → Settings → API Tokens, or programmatically. Every request needs this header — there's no unauthenticated read access to content by default, even for published items.
Response Format
Every response follows the same envelope, success or failure:
{ "success": true, "data": { /* ... */ } }{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message",
"details": { }
}
}Common error codes worth checking for explicitly in scripted usage:
- UNAUTHORIZED (401) — missing or invalid token.
- FORBIDDEN (403) — token valid, but insufficient role/scope for this action.
- VALIDATION_ERROR (400) — malformed input data.
- SLUG_CONFLICT (409) — the slug you're creating already exists.
- RESERVED_SLUG (400) — you tried to use a reserved field or content slug.
- NOT_FOUND (404) — the resource doesn't exist.
Content Endpoints
GET /_emdash/api/content/:collection # list, paginated
GET /_emdash/api/content/:collection/:id # get one
POST /_emdash/api/content/:collection # create
PUT /_emdash/api/content/:collection/:id # update
DELETE /_emdash/api/content/:collection/:id # delete (soft)List accepts `cursor`, `limit` (default 50), `status`, `orderBy`, and `order` as query parameters. A create request:
POST /_emdash/api/content/posts
Content-Type: application/json
{
"data": { "title": "New Post", "content": [ /* Portable Text blocks */ ] },
"slug": "new-post",
"status": "draft"
}Media Endpoints
GET /_emdash/api/media # list, filter by mimeType
GET /_emdash/api/media/:id # get one
POST /_emdash/api/media # register metadata after upload
PUT /_emdash/api/media/:id # update alt text, caption, dimensions
DELETE /_emdash/api/media/:id # delete
GET /_emdash/api/media/file/:key # serve the actual file (local storage only)This endpoint records media metadata after upload. Use the upload endpoint or signed URLs to upload the actual file.
That's the one non-obvious part of the media API: `POST /_emdash/api/media` registers a metadata record for a file that's already been placed in storage — it doesn't accept a binary upload body itself. The actual bytes go through a signed upload URL flow (or the admin UI's drag-and-drop, which handles this for you).
Schema Endpoints
GET /_emdash/api/schema/collectionsLists every collection and its field definitions — useful for a script that needs to introspect a site's content model before creating entries, rather than hardcoding field names it might get wrong.
Search Endpoints
GET /_emdash/api/search?q=hello+world # global search across collections
GET /_emdash/api/search/suggest?q=hel&limit=5 # autocomplete
POST /_emdash/api/search/rebuild # rebuild the FTS index
GET /_emdash/api/search/stats # indexed document countsGlobal search accepts `collections` (comma-separated slugs to scope the search), `status` (defaults to `published`), `limit`, and `cursor`. Results include a highlighted `snippet` with `<mark>` tags around matched terms — ready to render directly if you strip or style that tag appropriately.
Revision Endpoints
GET /_emdash/api/content/:collection/:entryId/revisions
GET /_emdash/api/revisions/:revisionId
POST /_emdash/api/revisions/:revisionId/restoreRestoring a revision doesn't just roll back — it creates a new revision from the restored state, preserving the full history rather than deleting anything after the restore point.
Plugin Endpoints
GET /_emdash/api/admin/plugins
GET /_emdash/api/admin/plugins/:id
POST /_emdash/api/admin/plugins/:id/enable
POST /_emdash/api/admin/plugins/:id/disableAny plugin you install can also expose its own routes under `/_emdash/api/plugins/<slug>/<route-name>` — those aren't documented centrally since they're plugin-specific, but they follow the same auth and response-envelope conventions as core endpoints.
Optimistic Concurrency: The _rev Token
Content responses include a `_rev` token. Pass it back on update to prove you've seen the current state before overwriting it — this is what prevents two concurrent editors (or a script and a human) from silently clobbering each other's changes:
# 1. Read, note the _rev
curl .../content/posts/01ABC123 -H "Authorization: Bearer $TOKEN"
# 2. Update, passing that _rev back
curl -X PUT .../content/posts/01ABC123 \
-H "Authorization: Bearer $TOKEN" \
-d '{"data": {"title": "Updated"}, "_rev": "MToyMDI2LTAyLTE0..."}'If the item changed since your read, the server returns a 409 Conflict instead of silently overwriting — re-read and retry rather than assuming your write went through.
A Practical Alternative: The CLI
For scripting and CI, the `emdash` CLI wraps this same API with a friendlier interface — `npx emdash content create posts --file post.json`, `npx emdash content publish posts 01ABC123`, and so on — and handles the `_rev` conflict-detection dance for you on updates. Reach for the raw REST API when you need something the CLI doesn't cover, or when calling from a non-Node environment.
CORS for Browser-Based Requests
The API supports CORS for browser requests. Configure allowed origins in your deployment.
Relevant specifically if you're calling the API directly from client-side JavaScript in a separate application — server-to-server calls (the more common pattern) aren't subject to CORS at all.
Frequently Asked Questions
Is there a rate limit?
API endpoints may be rate-limited depending on your deployment configuration. A rate-limited response returns 429 with a `Retry-After` header telling you how long to wait before retrying.
Can I use this API from a completely separate application, not just scripts?
Technically yes, with CORS configured — but see our note on what that trade-off actually costs you compared to EmDash's native Astro integration before building a whole separate frontend around it.
Does creating content via the API auto-publish it?
Depends on which tool: the REST API's create endpoint respects whatever `status` you pass (defaulting to what you specify, typically `draft`); the CLI's `content create` auto-publishes unless you pass `--draft` explicitly — check which one you're using.
How do I find a collection's exact field names before writing a create request?
`GET /_emdash/api/schema/collections`, or `npx emdash schema get <collection>` via the CLI — both return the live field list so you're not guessing or working from stale documentation.
The Bottom Line
EmDash's REST API is comprehensive and consistent — one response envelope, one auth scheme, and the same endpoints the admin panel itself calls. For most scripting needs, the CLI is the faster path; reach for raw REST when you need CORS-based browser access or something outside the CLI's coverage. See our guide to customizing content models to understand what you're querying and creating against.




Comments