Posts API
The Posts API is the most commonly used surface — it's how n8n creates drafts, how mobile apps pull the feed, and how external publishers push content in. Every endpoint is under /api/v1/posts and requires a Bearer token with the relevant posts:* scope.
List posts
GET /api/v1/posts
Required scope: posts:read.
Query parameters:
status—draft,scheduled,published(default),archivedcategory— slug or IDauthor— user IDsearch— keyword search across title and contentpage— 1-indexed page number (default 1)per_page— items per page, max 100 (default 20)order_by—published_at,updated_at,title(defaultpublished_at)order—asc,desc(defaultdesc)
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://yoursite.com/api/v1/posts?status=published&category=nutrition&per_page=50"
Response:
{
"data": [
{
"id": 123,
"title": "10 high-protein breakfasts",
"slug": "high-protein-breakfasts",
"status": "published",
"excerpt": "Start your day with...",
"content_html": "<p>...</p>",
"content_markdown": "...",
"author": { "id": 1, "name": "Ada Lovelace" },
"categories": [ { "id": 3, "slug": "nutrition", "name": "Nutrition" } ],
"tags": [ "protein", "breakfast" ],
"featured_image": { "id": 45, "url": "https://.../cover.jpg", "width": 1600, "height": 900 },
"published_at": "2026-04-10T09:00:00Z",
"content_modified_at": "2026-05-02T14:22:00Z",
"reviewed_at": null,
"updated_at": "2026-04-10T09:00:00Z",
"url": "https://yoursite.com/blog/high-protein-breakfasts"
}
],
"pagination": { "page": 1, "per_page": 50, "total": 237, "total_pages": 5 }
}
Date field semantics: published_at is set once, at first publish, and never changes afterward. content_modified_at is the last time the reader-facing content actually changed (title, body, excerpt, or featured image) — this is the field sitemap lastmod and structured-data dateModified are based on, so it's the one to watch if you're syncing external caches or search indexes. reviewed_at marks the last editorial review pass and is independent of content edits — it's null until an editor explicitly marks the post reviewed. updated_at is an internal system-touch signal (view counters, cache rebuilds, bulk SEO jobs) and should not be treated as a content-change indicator. Both content_modified_at and reviewed_at return null if unset, or if the installation predates the editorial date model.
Get a single post
GET /api/v1/posts/{id}
Required scope: posts:read.
Returns the same shape as the list items but with full content_html and content_markdown. Accepts either the numeric ID or the slug:
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://yoursite.com/api/v1/posts/high-protein-breakfasts
Create a post
POST /api/v1/posts
Content-Type: application/json
Required scope: posts:write.
Request body:
{
"title": "My new post",
"content_markdown": "# Hello\n\nThis is the body...",
"status": "draft",
"categories": [ "nutrition" ],
"tags": [ "protein", "breakfast" ],
"featured_image_id": 45,
"published_at": null,
"seo": {
"title": "Optional SEO title",
"description": "Optional SEO description"
}
}
Required fields: title, content_markdown (or content_html). All others optional.
Valid status values: draft, scheduled, published. If scheduled, you must supply a future published_at.
Response: 201 Created with the full post object (same shape as GET). The id, slug, content_html (rendered), and url fields are populated server-side.
Update a post
PUT /api/v1/posts/{id}
Content-Type: application/json
Required scope: posts:write.
Accepts the same body as create. Partial updates work — send only the fields you want to change. Unspecified fields stay untouched.
To move a draft to published:
{ "status": "published" }
Response: 200 OK with the updated post.
Delete a post
DELETE /api/v1/posts/{id}
Required scope: posts:write.
Default: soft delete (sets status to archived, hides from public). Pass ?hard=true for a permanent delete (removes the DB row; media and comments remain).
Response: 204 No Content.
Error codes
| Code | Meaning | |------|---------| | 400 | Invalid request body (missing required field, wrong type, invalid status) | | 401 | Missing / invalid / expired Bearer token | | 403 | Token lacks required scope, or IP restricted | | 404 | Post not found | | 409 | Slug collision on create (auto-resolved by server unless you override) | | 422 | Validation failed (field-level errors) | | 429 | Rate limit exceeded — respect Retry-After header | | 500 | Server error — include the X-Request-Id header value if reporting to support |
Error response body:
{
"error": "validation_failed",
"message": "one or more fields are invalid",
"fields": {
"title": "required",
"status": "must be one of: draft, scheduled, published"
}
}
n8n integration example — create draft from webhook
A complete n8n flow that accepts an inbound webhook with { title, content }, calls the jekcms API to create a draft, and returns the new post URL:
- Webhook node — method POST, path
/new-draft - HTTP Request node:
- Method: POST - URL: https://yoursite.com/api/v1/posts - Authentication: Header Auth → name Authorization, value Bearer {{$credentials.jekcmsToken}} - Body content type: JSON - Body:
{
"title": "={{ $json.title }}",
"content_markdown": "={{ $json.content }}",
"status": "draft",
"categories": ["auto-import"],
"tags": ["from-webhook"]
}
- Respond to Webhook node — return
{{ $node["HTTP Request"].json.url }}so the caller gets the draft preview URL immediately
Turn on Retry on fail in the HTTP Request node (3 attempts, exponential backoff) so transient network errors don't drop submissions.