API Reference
Complete REST API documentation for the Contento CMS.
Overview
Contento exposes a comprehensive REST API with 135 endpoints across multiple groups. All endpoints include OpenAPI metadata and are documented in the interactive Scalar API explorer.
- Authentication API (
/api/v1/auth) — Token-based authentication - Content Delivery API (
/api/v1/content) — Public, read-only endpoints for frontend consumption - Management API (
/api/v1) — Authenticated CRUD endpoints for managing content - Newsletter API (
/api/v1/newsletter) — Subscriber and campaign management - Membership API (
/api/v1/membership) — Stripe-powered subscriptions and billing - AI API (
/api/v1/ai) — AI-powered content generation and analysis - Sites API (
/api/v1/sites) — Multi-site management - Scheduler API (
/api/v1/scheduler) — Task scheduling and automation - Redirects API (
/api/v1/redirects) — URL redirect management with hit tracking - MCP Integration — Guide for using the API with AI agents via Model Context Protocol
Base URL
https://your-contento-site.com/api/v1
Response format
All successful responses follow this envelope:
{
"data": { ... } // Single object or array
}
// With pagination:
{
"data": [ ... ],
"meta": {
"page": 1,
"pageSize": 20,
"totalCount": 42,
"totalPages": 3
}
}
Error format
{
"error": {
"code": "NOT_FOUND",
"message": "Post not found."
}
}
Pagination
Paginated endpoints accept these query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
pageSize | integer | 20 | Items per page |
Authentication
Management API endpoints require a bearer token in the Authorization header:
Authorization: Bearer <your-token>
Tokens are HMAC-SHA256 signed and expire after 24 hours. See the Authentication API below.
Authentication API
Base path: /api/v1/auth — All endpoints are public (no token required to call).
Exchange email and password for a bearer token.
Request body
{
"email": "admin@contento.local",
"password": "Admin1234"
}
Response 200 OK
{
"data": {
"token": "eyJVc2VySWQ...signature",
"tokenType": "Bearer",
"expiresAt": "2026-02-19T12:00:00Z",
"expiresIn": 86400,
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "admin@contento.local",
"displayName": "Admin",
"role": "owner"
}
}
}
Error responses
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_REQUEST | Email and password are required. |
| 401 | INVALID_CREDENTIALS | Invalid email or password. |
| 401 | ACCOUNT_DISABLED | This account has been disabled. |
Refresh an existing valid token. Returns a new token with a fresh 24-hour expiry.
Request body
{
"token": "eyJVc2VySWQ...signature"
}
Response 200 OK
{
"data": {
"token": "eyJVc2VySWQ...newSignature",
"tokenType": "Bearer",
"expiresAt": "2026-02-20T12:00:00Z",
"expiresIn": 86400
}
}
Error responses
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_REQUEST | Token is required. |
| 401 | INVALID_TOKEN | Token is invalid or expired. |
Validate a token and return the associated user profile. Requires Authorization: Bearer <token> header.
Response 200 OK
{
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "admin@contento.local",
"displayName": "Admin",
"role": "owner",
"isActive": true
}
}
Error responses
| Status | Code | Description |
|---|---|---|
| 401 | NO_TOKEN | Bearer token required. |
| 401 | INVALID_TOKEN | Token is invalid or expired. |
| 404 | NOT_FOUND | User not found. |
Register a new user account. Creates a viewer-role user and sends an email verification link.
Request body
{
"email": "user@example.com",
"password": "securePassword123",
"displayName": "Jane Doe"
}
Response 201 Created
{
"data": { "message": "Account created. Please check your email to verify." }
}
Request a password reset email. Always returns success to prevent email enumeration.
Request body
{
"email": "user@example.com"
}
Reset a user’s password using a valid reset token (1-hour expiry).
Request body
{
"token": "reset-token-from-email",
"newPassword": "newSecurePassword456"
}
Verify a user’s email address using the token sent during registration.
Request body
{
"token": "verification-token-from-email"
}
Content Delivery API
Base path: /api/v1/content — All endpoints are public (no authentication required). Responses are cached for performance.
Get site metadata, active theme CSS variables, default layout, and category tree for navigation. Cached for 5 minutes.
Response 200 OK
{
"data": {
"site": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "My Blog",
"slug": "default",
"tagline": "Thoughts on software engineering",
"locale": "en-US",
"timezone": "UTC",
"customDomain": null,
"settings": {}
},
"theme": {
"id": "b1c2d3e4-f5a6-7890-bcde-fa1234567890",
"name": "Shizen",
"slug": "shizen",
"cssVariables": "{\"--font-body\":\"Noto Serif JP\",\"--color-accent\":\"#3D5A80\"}"
},
"defaultLayout": {
"id": "c1d2e3f4-a5b6-7890-cdef-ab1234567890",
"name": "Standard Blog",
"slug": "standard-blog",
"structure": "{\"regions\":[\"header\",\"main\",\"sidebar\",\"footer\"]}"
},
"navigation": [
{
"id": "...",
"name": "Technology",
"slug": "technology",
"parentId": null,
"sortOrder": 0
}
]
}
}
List published posts with pagination and optional filters. Cached for 1 minute.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
category | string | — | Filter by category slug |
tag | string | — | Filter by tag name |
featured | boolean | — | If true, only featured posts |
page | integer | 1 | Page number |
pageSize | integer | 20 | Items per page |
Response 200 OK
{
"data": [
{
"id": "d1e2f3a4-b5c6-7890-def0-123456789abc",
"title": "Getting Started with Contento",
"subtitle": "A beginner's guide",
"slug": "getting-started-with-contento",
"excerpt": "Learn how to set up and configure Contento CMS...",
"coverImageUrl": "/uploads/hero.jpg",
"status": "published",
"featured": true,
"publishedAt": "2026-02-15T10:30:00Z",
"wordCount": 1240,
"readingTimeMinutes": 5,
"tags": ["tutorial", "getting-started"],
"author": { "authorId": "a1b2c3d4-..." }
}
],
"meta": {
"page": 1,
"pageSize": 20,
"totalCount": 42,
"totalPages": 3
}
}
Get a single published post by slug with full rendered HTML, markdown source, and auto-generated table of contents.
Path parameters
| Parameter | Type | Description |
|---|---|---|
slug | string | The post's URL slug |
Response 200 OK
{
"data": {
"id": "d1e2f3a4-b5c6-7890-def0-123456789abc",
"title": "Getting Started with Contento",
"subtitle": "A beginner's guide",
"slug": "getting-started-with-contento",
"excerpt": "Learn how to set up and configure Contento CMS...",
"bodyMarkdown": "# Introduction\n\nContento is...",
"bodyHtml": "<h1 id=\"introduction\">Introduction</h1>\n<p>Contento is...</p>",
"coverImageUrl": "/uploads/hero.jpg",
"metaTitle": "Getting Started with Contento CMS",
"metaDescription": "Step-by-step guide to setting up Contento CMS",
"canonicalUrl": null,
"status": "published",
"visibility": "public",
"featured": true,
"publishedAt": "2026-02-15T10:30:00Z",
"wordCount": 1240,
"readingTimeMinutes": 5,
"tags": ["tutorial", "getting-started"],
"version": 3,
"author": { "authorId": "a1b2c3d4-..." },
"commentCount": 7,
"tableOfContents": [
{ "id": "introduction", "text": "Introduction", "level": 1 },
{ "id": "installation", "text": "Installation", "level": 2 },
{ "id": "configuration", "text": "Configuration", "level": 2 }
]
}
}
Get approved comments for a post, returned as a threaded tree. Cached for 30 seconds.
Response 200 OK
{
"data": [
{
"id": "e1f2a3b4-c5d6-7890-ef01-234567890abc",
"authorName": "Jane Doe",
"authorEmail": "jane@example.com",
"bodyMarkdown": "Great article! Very helpful.",
"bodyHtml": "<p>Great article! Very helpful.</p>",
"status": "approved",
"parentId": null,
"depth": 0,
"likesCount": 2,
"createdAt": "2026-02-16T14:22:00Z"
}
]
}
Submit a comment on a post. New comments start with pending status and must be approved by a moderator.
Request body
{
"authorName": "Jane Doe",
"authorEmail": "jane@example.com",
"body": "Great article! Very helpful.",
"parentId": null,
"website": "" // honeypot - leave empty
}
website field is a honeypot. It must be left empty.
If filled, the comment is silently discarded (returns 200 but does not save).
Response 200 OK
{
"data": {
"id": "f1a2b3c4-d5e6-7890-f012-345678901abc",
"status": "pending"
}
}
Full-text search across published posts. Returns results with highlighted title and excerpt showing matching terms.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | Required | Search query |
page | integer | 1 | Page number |
pageSize | integer | 20 | Items per page |
Response 200 OK
{
"data": [
{
"id": "d1e2f3a4-...",
"title": "Getting Started with Contento",
"slug": "getting-started-with-contento",
"excerpt": "Learn how to set up...",
"coverImageUrl": "/uploads/hero.jpg",
"publishedAt": "2026-02-15T10:30:00Z",
"readingTimeMinutes": 5,
"tags": ["tutorial"],
"highlightedTitle": "Getting Started with <mark>Contento</mark>",
"highlightedExcerpt": "Learn how to set up <mark>Contento</mark> CMS..."
}
],
"meta": {
"page": 1,
"pageSize": 20,
"totalCount": 3,
"totalPages": 1
}
}
Get autocomplete suggestions for a search query. Returns an array of suggested search terms.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | Required | Partial search query |
limit | integer | 5 | Maximum number of suggestions |
Response 200 OK
{
"data": [
"contento cms",
"contento api",
"contento themes"
]
}
Get the full category tree for navigation. Cached for 5 minutes.
Response 200 OK
{
"data": [
{
"id": "a1b2c3d4-...",
"name": "Technology",
"slug": "technology",
"description": "Posts about tech",
"parentId": null,
"sortOrder": 0
},
{
"id": "b2c3d4e5-...",
"name": "Web Development",
"slug": "web-development",
"description": null,
"parentId": "a1b2c3d4-...",
"sortOrder": 1
}
]
}
Get all available themes with their CSS variable definitions. Cached for 5 minutes.
Response 200 OK
{
"data": [
{
"id": "b1c2d3e4-...",
"name": "Shizen",
"slug": "shizen",
"description": "Nature-inspired minimalism",
"cssVariables": "{\"--font-body\":\"Noto Serif JP\",...}",
"isActive": true
}
]
}
Get all available layouts with their JSON structure definitions. Cached for 5 minutes.
Response 200 OK
{
"data": [
{
"id": "c1d2e3f4-...",
"name": "Standard Blog",
"slug": "standard-blog",
"description": "Classic two-column blog layout",
"structure": "{\"regions\":[...],\"grid\":\"2fr 1fr\"}",
"isDefault": true
}
]
}
List media files with optional type filtering. Cached for 1 minute.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | — | Filter by MIME type prefix (e.g., image, video) |
page | integer | 1 | Page number |
pageSize | integer | 30 | Items per page |
Response 200 OK
{
"data": [
{
"id": "e1f2a3b4-...",
"filename": "a1b2c3d4e5f6.jpg",
"originalName": "hero-image.jpg",
"mimeType": "image/jpeg",
"fileSize": 245760,
"width": 1920,
"height": 1080,
"altText": "Hero banner image",
"storagePath": "/uploads/a1b2c3d4e5f6.jpg",
"createdAt": "2026-02-14T09:00:00Z"
}
],
"meta": {
"page": 1,
"pageSize": 30,
"totalCount": 15,
"totalPages": 1
}
}
Get the navigation menu tree for a display location (e.g. header, footer). Returns the menu metadata and a nested tree of items with resolved URLs. Cached for 5 minutes.
Path parameters
| Parameter | Type | Description |
|---|---|---|
location | string | Menu location: header, footer, or a custom value |
Response (200)
{
"data": {
"menu": {
"id": "a1b2c3d4-...",
"name": "Main Navigation",
"slug": "main-navigation",
"location": "header"
},
"items": [
{
"id": "e5f6a7b8-...",
"label": "Home",
"url": "/",
"target": "_self",
"children": []
},
{
"id": "c9d0e1f2-...",
"label": "Articles",
"url": "#",
"target": "_self",
"children": [
{
"id": "d1e2f3a4-...",
"label": "Technology",
"url": "/category/technology",
"target": "_self",
"children": []
}
]
}
]
}
}
Management API — Posts
Base path: /api/v1/posts — Requires authentication.
List all posts (any status) with filtering and pagination.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: draft, published, scheduled |
q | string | — | Search query (title, body) |
page | integer | 1 | Page number |
pageSize | integer | 20 | Items per page |
Response 200 OK
{
"data": [ /* array of Post objects */ ],
"meta": { "page": 1, "pageSize": 20, "totalCount": 42, "totalPages": 3 }
}
Create a new post.
Request body
{
"title": "My New Post",
"subtitle": "An optional subtitle",
"bodyMarkdown": "# Hello World\n\nThis is my first post.",
"coverImageUrl": "/uploads/cover.jpg",
"status": "draft",
"visibility": "public",
"featured": false,
"metaTitle": "My New Post - SEO Title",
"metaDescription": "SEO description for the post",
"canonicalUrl": null,
"tags": ["tutorial", "dotnet"]
}
Response 201 Created
{
"data": { /* full Post object with generated id and slug */ }
}
Get a single post by ID (any status).
Response 200 OK
{
"data": { /* full Post object */ }
}
Get a single post by slug (any status). Useful for admin preview of draft posts.
Update an existing post. Only send the fields you want to change (partial update).
Request body (all fields optional)
{
"title": "Updated Title",
"subtitle": "Updated subtitle",
"slug": "updated-slug",
"bodyMarkdown": "# Updated content",
"coverImageUrl": "/uploads/new-cover.jpg",
"status": "published",
"visibility": "public",
"featured": true,
"metaTitle": "Updated SEO Title",
"metaDescription": "Updated SEO description",
"canonicalUrl": "https://example.com/original",
"tags": ["updated", "tags"]
}
status to "published" on a draft post automatically
sets publishedAt to the current UTC timestamp.
Permanently delete a post. Returns 204 No Content on success.
Publish a draft or scheduled post. Sets status to published and publishedAt to now.
Response 200 OK
{
"data": { /* updated Post object with status: "published" */ }
}
Revert a published post to draft. Removes it from the public site.
Response 200 OK
{
"data": { /* updated Post object with status: "draft" */ }
}
Management API — Categories
Base path: /api/v1/categories — Requires authentication.
List all categories with pagination.
Query parameters
| Parameter | Type | Default |
|---|---|---|
page | integer | 1 |
pageSize | integer | 100 |
Get the full category hierarchy as a tree structure.
Get a single category by ID.
Create a new category.
Request body
{
"name": "Web Development",
"slug": "web-development",
"description": "Posts about web dev technologies",
"parentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sortOrder": 1
}
Response 201 Created
{
"data": { /* Category object */ }
}
Update a category. All fields are optional (partial update).
Request body (all fields optional)
{
"name": "Updated Name",
"slug": "updated-slug",
"description": "Updated description",
"parentId": null,
"sortOrder": 2
}
Delete a category. Returns 204 No Content.
Management API — Comments
Base path: /api/v1 — Requires authentication (except creating comments).
List all comments for a post (all statuses, threaded). Used by the admin moderation UI.
Create a new comment (public, no auth required). Includes honeypot spam protection.
Request body
{
"postId": "d1e2f3a4-b5c6-7890-def0-123456789abc",
"authorName": "Jane Doe",
"authorEmail": "jane@example.com",
"bodyMarkdown": "Great post!",
"parentId": null,
"website": "" // honeypot - must be empty
}
Approve a pending comment, making it visible on the public post.
Response 200 OK
{
"data": { "status": "approved" }
}
Mark a comment as spam.
Response 200 OK
{
"data": { "status": "spam" }
}
Trash/delete a comment. Returns 204 No Content.
Management API — Media
Base path: /api/v1/media — Requires authentication.
List all media files with pagination.
Query parameters
| Parameter | Type | Default |
|---|---|---|
page | integer | 1 |
pageSize | integer | 20 |
Upload a file. Uses multipart/form-data with a file field.
Request
# Using curl: curl -X POST https://your-site.com/api/v1/media/upload \ -H "Authorization: Bearer <token>" \ -F "file=@/path/to/image.jpg"
Constraints
| Constraint | Value |
|---|---|
| Max file size | 10 MB |
| Allowed MIME types | image/jpeg, image/png, image/gif, image/webp, image/svg+xml, application/pdf, video/mp4 |
Response 200 OK
{
"data": {
"id": "e1f2a3b4-c5d6-7890-ef01-234567890abc",
"url": "/uploads/a1b2c3d4e5f6.jpg",
"filename": "image.jpg",
"mimeType": "image/jpeg",
"fileSize": 245760
}
}
Error responses
| Status | Code | Description |
|---|---|---|
| 400 | NO_FILE | No file provided. |
| 400 | FILE_TOO_LARGE | File exceeds 10MB limit. |
| 400 | INVALID_TYPE | File type not allowed. |
Delete a media file. Returns 204 No Content.
Management API — Layouts
Base path: /api/v1/layouts — Requires authentication.
List all layouts with pagination.
Get a layout with its components.
Response 200 OK
{
"data": {
"layout": { /* Layout object */ },
"components": [ /* array of LayoutComponent objects */ ]
}
}
Create a new layout.
Request body
{
"name": "Custom Layout",
"slug": "custom-layout",
"description": "A custom page layout",
"isDefault": false,
"structure": "{\"regions\":[\"header\",\"main\",\"footer\"],\"grid\":\"1fr\"}",
"headContent": "<link rel=\"stylesheet\" href=\"...\">",
"customCss": ".custom { color: red; }",
"customJs": null
}
Update a layout. All fields optional (partial update).
Delete a layout. Cannot delete the default layout. Returns 204 No Content.
Set a layout as the default. The previous default is unset automatically.
Management API — Menus
Base path: /api/v1/menus — Requires authentication.
List all navigation menus for the current site.
Create a new navigation menu.
Request body
| Field | Type | Description |
|---|---|---|
name | string | Display name for the menu |
slug | string? | URL slug (auto-generated from name if omitted) |
location | string? | header, footer, or custom. Defaults to header |
isActive | bool? | Whether the menu is active. Defaults to true |
Get a menu with its items (flat list and rendered tree).
Update a menu’s name, location, or active status.
Delete a menu and all its items.
Add a new item to a menu.
Request body
| Field | Type | Description |
|---|---|---|
label | string | Display text for the link |
url | string? | URL for custom links |
linkType | string? | custom, post, category, or tag |
linkId | guid? | Referenced post or category ID (when linkType is not custom) |
target | string? | _self or _blank |
parentId | guid? | Parent item ID for nesting (dropdown) |
sortOrder | int? | Display order |
Update an existing menu item.
Remove an item from a menu. Child items are re-parented to the deleted item’s parent.
Reorder items by providing an ordered array of item IDs.
Request body
{
"itemIds": ["guid-1", "guid-2", "guid-3"]
}
Management API — Redirects
Base path: /api/v1/redirects — Requires authentication.
List all redirects for the current site with pagination metadata.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
pageSize | integer | 50 | Items per page |
Response 200 OK
{
"data": [
{
"id": "guid",
"fromPath": "/old-slug",
"toPath": "/new-slug",
"statusCode": 301,
"hitCount": 42,
"lastHitAt": "2026-02-19T...",
"isActive": true,
"notes": "Post slug changed"
}
],
"meta": { "page": 1, "pageSize": 50, "totalCount": 12, "totalPages": 1 }
}
Get a redirect by ID including hit count and last-hit timestamp.
Create a new 301 or 302 redirect.
Request body
{
"fromPath": "/old-page",
"toPath": "/new-page",
"statusCode": 301,
"notes": "Page moved permanently",
"isActive": true
}
Response 201 Created
{
"data": { /* Redirect object */ }
}
Update an existing redirect’s paths, status code, notes, or active state.
Permanently delete a redirect. Returns 204 No Content.
Management API — Plugins
Base path: /api/v1/plugins — Requires authentication.
List all installed plugins.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabledOnly | boolean | — | If true, only return enabled plugins |
page | integer | 1 | Page number |
pageSize | integer | 50 | Items per page |
Install a new plugin.
Request body
{
"name": "Reading Progress Bar",
"slug": "reading-progress",
"version": "1.0.0",
"author": "Contento",
"description": "Adds a reading progress bar to posts",
"entryPoint": "(function() { ... })();",
"permissions": ["post:render"],
"settings": "{\"color\":\"#3D5A80\",\"height\":\"3px\"}"
}
Response 201 Created
{
"data": { /* InstalledPlugin object */ }
}
Get a single plugin by ID, including its settings and entry point code.
Enable a disabled plugin.
Disable an enabled plugin without uninstalling it.
Update a plugin's settings JSON.
Request body
{
"settings": "{\"color\":\"#DC2626\",\"height\":\"4px\"}"
}
Response 200 OK
{
"data": { "settings": "{\"color\":\"#DC2626\",\"height\":\"4px\"}" }
}
Uninstall and permanently remove a plugin. Returns 204 No Content.
Management API — Search
Base path: /api/v1/search — Requires authentication.
Full-text search across all posts (including drafts). Requires the q parameter.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | Required | Search query |
page | integer | 1 | Page number |
pageSize | integer | 20 | Items per page |
Get search autocomplete suggestions.
Query parameters
| Parameter | Type | Default |
|---|---|---|
q | string | Required |
limit | integer | 5 |
Management API — Import/Export
Base path: /api/v1/import-export — Requires authentication.
Import content from a WordPress WXR export file. Uses multipart/form-data.
Request
curl -X POST https://your-site.com/api/v1/import-export/wordpress \ -H "Authorization: Bearer <token>" \ -F "file=@/path/to/export.xml"
Accepted file extensions: .xml, .wxr
Import posts from uploaded markdown files. Uses multipart/form-data with multiple files.
Request
curl -X POST https://your-site.com/api/v1/import-export/markdown \ -H "Authorization: Bearer <token>" \ -F "files=@post1.md" \ -F "files=@post2.md"
Accepted file extensions: .md, .markdown
Export the entire site as JSON. Returns application/json.
Export a single post as markdown with YAML front matter. Returns text/markdown.
Example response
--- title: "Getting Started with Contento" slug: "getting-started-with-contento" date: "2026-02-15T10:30:00Z" tags: - tutorial - getting-started --- # Introduction Contento is...
Export a single post as rendered HTML. Returns text/html.
Management API — Site
Base path: /api/v1/site — Requires authentication.
Get the current site configuration.
Response 200 OK
{
"data": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "My Blog",
"slug": "default",
"tagline": "Thoughts on software engineering",
"locale": "en-US",
"timezone": "UTC",
"settings": {},
"themeId": "b1c2d3e4-...",
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-02-18T12:00:00Z"
}
}
Management API — Themes
Base path: /api/v1/themes — Requires authentication.
List all themes with pagination.
Get a single theme by ID with full CSS variables.
Create a new theme.
Request body
{
"name": "Custom Theme",
"slug": "custom-theme",
"description": "A custom theme",
"cssVariables": "{\"--font-body\":\"Inter\",\"--color-accent\":\"#3D5A80\"}"
}
Update a theme. All fields optional (partial update).
Set a theme as the active theme for the site.
Newsletter API
Base path: /api/v1/newsletter — Requires authentication (except subscriber signup).
List all subscribers with status filtering and pagination.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter: active, unsubscribed |
page | integer | 1 | Page number |
pageSize | integer | 50 | Items per page |
Subscribe a new email address. Public endpoint (no auth required).
Request body
{
"email": "reader@example.com"
}
Unsubscribe an email address. Public endpoint.
List all newsletter campaigns with status and pagination.
Create a new campaign.
Request body
{
"subject": "Weekly Digest",
"body": "<h1>This week...</h1>",
"status": "draft"
}
Membership API
Base path: /api/v1/membership — Mix of public and authenticated endpoints.
Create a Stripe Checkout session for a membership plan. Public endpoint.
Request body
{
"email": "reader@example.com",
"priceId": "price_1234567890"
}
Response 200 OK
{
"data": {
"url": "https://checkout.stripe.com/c/pay/cs_live_..."
}
}
Error responses
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_EMAIL | Email is required. |
| 400 | NOT_CONFIGURED | Stripe is not configured. |
Create a Stripe Billing Portal session for managing subscriptions. Requires authentication.
Request body
{
"stripeCustomerId": "cus_1234567890"
}
Response 200 OK
{
"data": {
"url": "https://billing.stripe.com/p/session/..."
}
}
Check membership status for an email address. Public endpoint.
Query parameters
| Parameter | Type | Description |
|---|---|---|
email | string | Email address to check |
Response 200 OK
{
"data": {
"active": true,
"tier": "premium"
}
}
AI API
Base path: /api/v1/ai — Requires authentication. AI must be enabled in site settings with a configured API key.
Send a prompt to the configured AI provider and get a text response. Useful for content generation, rewriting, and summarization.
Request body
{
"systemPrompt": "You are a helpful writing assistant.",
"userPrompt": "Write an introduction about sustainable web design"
}
Response 200 OK
{
"data": {
"text": "Sustainable web design is an approach...",
"tokensUsed": 342
}
}
Error responses
| Status | Code | Description |
|---|---|---|
| 400 | AI_DISABLED | AI is not configured. Set up your API key in Settings > AI. |
| 400 | AI_ERROR | The AI provider returned an error. |
Stream AI-generated text as Server-Sent Events (SSE). Each chunk arrives as it is generated for real-time display.
Request body
{
"systemPrompt": "You are a helpful writing assistant. Respond in markdown.",
"userPrompt": "Write a blog post about container gardening"
}
Response (SSE stream)
Content-Type: text/event-stream
data: {"text": "# Container"}
data: {"text": " Gardening"}
data: {"text": " for Beginners\n\n"}
data: [DONE]
Generate a complete CSS theme from a natural language description. Returns CSS variables and theme configuration.
Request body
{
"prompt": "A warm autumn theme with earthy tones and serif fonts"
}
Generate a page layout structure from a natural language description. Returns a layout JSON configuration.
Request body
{
"prompt": "A magazine-style layout with featured hero and two-column grid"
}
MCP Integration
Contento's REST API is designed for Model Context Protocol (MCP) compatibility. AI agents can use the full API surface to autonomously create, manage, and publish content.
Why Contento works with MCP
- Stateless bearer tokens — No cookies or sessions. Agents authenticate once and include the token in every request.
- Full OpenAPI spec — Available at
/openapi/v1.json. Agents can discover all 135 endpoints, request/response schemas, and parameter types automatically. - Structured JSON responses — Every endpoint returns a consistent
{ data, meta }envelope that agents can parse reliably. - Complete CRUD coverage — Posts, categories, media, menus, themes, layouts, comments, redirects — every content type has full API support.
- Built-in AI endpoints — Text completion, streaming, and generative theme/layout creation are available as first-party API calls.
Agent workflow example
A typical MCP agent workflow for content generation:
// 1. Authenticate — get a bearer token POST /api/v1/auth/token { "email": "agent@site.com", "password": "..." } // → { data: { token: "eyJ...", expiresIn: 86400 } } // 2. Create a post POST /api/v1/posts Authorization: Bearer eyJ... { "title": "10 Tips for Better SEO", "bodyMarkdown": "# Introduction\n\n...", "tags": ["seo", "marketing"], "metaTitle": "10 SEO Tips for 2026", "metaDescription": "Actionable SEO tips...", "status": "draft" } // → { data: { id: "a1b2c3...", slug: "10-tips-for-better-seo" } } // 3. Upload a cover image POST /api/v1/media/upload (multipart/form-data) // → { data: { storagePath: "/uploads/cover.webp" } } // 4. Attach cover image and publish PUT /api/v1/posts/a1b2c3... { "coverImageUrl": "/uploads/cover.webp" } POST /api/v1/posts/a1b2c3.../publish // → 200 OK — post is live
MCP tool surface
The following API groups map directly to MCP tools:
| Tool Category | API Endpoints | Capabilities |
|---|---|---|
| Content | /api/v1/posts | Create, update, delete, publish, unpublish, schedule, preview, SEO analysis |
| Media | /api/v1/media | Upload, bulk upload, crop, update metadata, organize into folders |
| Categories | /api/v1/categories | Create hierarchical categories, reorder, assign to posts |
| Navigation | /api/v1/menus | Create menus, add/remove/reorder items, assign to locations |
| Search | /api/v1/content/search | Full-text search, autocomplete suggestions (public, no auth) |
| AI | /api/v1/ai | Text completion, streaming, theme generation, layout generation |
| Discovery | /api/v1/content | Read published posts, categories, themes, layouts, menus (public) |
| Auth | /api/v1/auth | Token exchange, refresh, user info |
/scalar to browse all endpoints interactively. The OpenAPI spec at /openapi/v1.json can be used by MCP servers to auto-generate tool definitions.
Sites API
Base path: /api/v1/sites — Requires authentication.
List all sites in the installation.
Create a new site.
Request body
{
"name": "My Second Blog",
"slug": "second-blog",
"domain": "blog2.example.com"
}
Get a single site by ID.
Update a site's configuration.
Delete a site. Cannot delete the primary site. Returns 204 No Content.
Set a site as the primary site. The previous primary is unset automatically.
Scheduler API
Base path: /api/v1/scheduler — Requires authentication.
List all scheduled tasks with their status and timing information.
Create a new scheduled task.
Request body
{
"name": "Clear Cache",
"taskType": "cache_clear",
"cronExpression": "0 * * * *",
"isEnabled": true
}
Get a single task with full details.
Update a scheduled task. All fields optional.
Delete a scheduled task. Returns 204 No Content.
Manually trigger a scheduled task to run immediately.
Run all tasks that are due. Used by the background service.
Post Types API
Base path: /api/v1/post-types — Requires authentication.
List all custom post types.
Create a new post type.
Request body
{
"name": "Recipe",
"slug": "recipe",
"icon": "chef-hat",
"fieldSchema": "{\"ingredients\":\"array\",\"cookTime\":\"number\"}"
}
Get a single post type by ID.
Update a post type. All fields optional.
Delete a post type. Returns 204 No Content.
Stripe Webhook
Endpoint: POST /api/v1/stripe/webhook — Called by Stripe. Signature verification required.
Receives Stripe webhook events. Verifies the Stripe-Signature header and processes events idempotently (using the StripeEvent table to prevent duplicate processing).
Handled events
| Event | Action |
|---|---|
checkout.session.completed | Activates membership for the subscriber |
customer.subscription.updated | Updates membership tier and expiry |
customer.subscription.deleted | Downgrades member to free tier |
invoice.payment_failed | Tracks payment failures, downgrades after 3 strikes |
Stripe:WebhookSecret environment variable to your Stripe webhook signing secret.
SEO & Feeds
Public endpoints for search engines and feed readers. No authentication required.
Sitemap index with separate post and category sitemaps. Generated by the SEO service with priority and change frequency.
RSS 2.0 feed of recent published posts.
Auto-generated robots.txt with sitemap reference.
Machine-readable site description for AI crawlers and LLMs.
/scalar. Each endpoint includes
EndpointSummary and EndpointDescription metadata for rich OpenAPI documentation.