Docs API Reference

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.

  1. Authentication API (/api/v1/auth) — Token-based authentication
  2. Content Delivery API (/api/v1/content) — Public, read-only endpoints for frontend consumption
  3. Management API (/api/v1) — Authenticated CRUD endpoints for managing content
  4. Newsletter API (/api/v1/newsletter) — Subscriber and campaign management
  5. Membership API (/api/v1/membership) — Stripe-powered subscriptions and billing
  6. AI API (/api/v1/ai) — AI-powered content generation and analysis
  7. Sites API (/api/v1/sites) — Multi-site management
  8. Scheduler API (/api/v1/scheduler) — Task scheduling and automation
  9. Redirects API (/api/v1/redirects) — URL redirect management with hit tracking
  10. 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:

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed)
pageSizeinteger20Items 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).

POST /api/v1/auth/token

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

StatusCodeDescription
400INVALID_REQUESTEmail and password are required.
401INVALID_CREDENTIALSInvalid email or password.
401ACCOUNT_DISABLEDThis account has been disabled.
POST /api/v1/auth/refresh

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

StatusCodeDescription
400INVALID_REQUESTToken is required.
401INVALID_TOKENToken is invalid or expired.
GET /api/v1/auth/me

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

StatusCodeDescription
401NO_TOKENBearer token required.
401INVALID_TOKENToken is invalid or expired.
404NOT_FOUNDUser not found.
POST /api/v1/auth/register

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." }
}
POST /api/v1/auth/forgot-password

Request a password reset email. Always returns success to prevent email enumeration.

Request body

{
  "email": "user@example.com"
}
POST /api/v1/auth/reset-password

Reset a user’s password using a valid reset token (1-hour expiry).

Request body

{
  "token": "reset-token-from-email",
  "newPassword": "newSecurePassword456"
}
POST /api/v1/auth/verify-email

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 /api/v1/content/site

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
      }
    ]
  }
}
GET /api/v1/content/posts

List published posts with pagination and optional filters. Cached for 1 minute.

Query parameters

ParameterTypeDefaultDescription
categorystring—Filter by category slug
tagstring—Filter by tag name
featuredboolean—If true, only featured posts
pageinteger1Page number
pageSizeinteger20Items 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 /api/v1/content/posts/{slug}

Get a single published post by slug with full rendered HTML, markdown source, and auto-generated table of contents.

Path parameters

ParameterTypeDescription
slugstringThe 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 /api/v1/content/posts/{slug}/comments

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"
    }
  ]
}
POST /api/v1/content/posts/{slug}/comments

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
}
Honeypot field: The 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"
  }
}
GET /api/v1/content/search

Full-text search across published posts. Returns results with highlighted title and excerpt showing matching terms.

Query parameters

ParameterTypeDefaultDescription
qstringRequiredSearch query
pageinteger1Page number
pageSizeinteger20Items 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 /api/v1/content/search/suggestions

Get autocomplete suggestions for a search query. Returns an array of suggested search terms.

Query parameters

ParameterTypeDefaultDescription
qstringRequiredPartial search query
limitinteger5Maximum number of suggestions

Response 200 OK

{
  "data": [
    "contento cms",
    "contento api",
    "contento themes"
  ]
}
GET /api/v1/content/categories

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 /api/v1/content/themes

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 /api/v1/content/layouts

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
    }
  ]
}
GET /api/v1/content/media

List media files with optional type filtering. Cached for 1 minute.

Query parameters

ParameterTypeDefaultDescription
typestring—Filter by MIME type prefix (e.g., image, video)
pageinteger1Page number
pageSizeinteger30Items 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 /api/v1/content/menu/{location}

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

ParameterTypeDescription
locationstringMenu 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.

GET /api/v1/posts

List all posts (any status) with filtering and pagination.

Query parameters

ParameterTypeDefaultDescription
statusstring—Filter by status: draft, published, scheduled
qstring—Search query (title, body)
pageinteger1Page number
pageSizeinteger20Items per page

Response 200 OK

{
  "data": [ /* array of Post objects */ ],
  "meta": { "page": 1, "pageSize": 20, "totalCount": 42, "totalPages": 3 }
}
POST /api/v1/posts

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 /api/v1/posts/{id}

Get a single post by ID (any status).

Response 200 OK

{
  "data": { /* full Post object */ }
}
GET /api/v1/posts/by-slug/{slug}

Get a single post by slug (any status). Useful for admin preview of draft posts.

PUT /api/v1/posts/{id}

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"]
}
Auto-publish: Setting status to "published" on a draft post automatically sets publishedAt to the current UTC timestamp.
DELETE /api/v1/posts/{id}

Permanently delete a post. Returns 204 No Content on success.

POST /api/v1/posts/{id}/publish

Publish a draft or scheduled post. Sets status to published and publishedAt to now.

Response 200 OK

{
  "data": { /* updated Post object with status: "published" */ }
}
POST /api/v1/posts/{id}/unpublish

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.

GET /api/v1/categories

List all categories with pagination.

Query parameters

ParameterTypeDefault
pageinteger1
pageSizeinteger100
GET /api/v1/categories/tree

Get the full category hierarchy as a tree structure.

GET /api/v1/categories/{id}

Get a single category by ID.

POST /api/v1/categories

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 */ }
}
PUT /api/v1/categories/{id}

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 /api/v1/categories/{id}

Delete a category. Returns 204 No Content.

Management API — Comments

Base path: /api/v1 — Requires authentication (except creating comments).

GET /api/v1/posts/{postId}/comments

List all comments for a post (all statuses, threaded). Used by the admin moderation UI.

POST /api/v1/comments

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
}
POST /api/v1/comments/{id}/approve

Approve a pending comment, making it visible on the public post.

Response 200 OK

{
  "data": { "status": "approved" }
}
POST /api/v1/comments/{id}/spam

Mark a comment as spam.

Response 200 OK

{
  "data": { "status": "spam" }
}
DELETE /api/v1/comments/{id}

Trash/delete a comment. Returns 204 No Content.

Management API — Media

Base path: /api/v1/media — Requires authentication.

GET /api/v1/media

List all media files with pagination.

Query parameters

ParameterTypeDefault
pageinteger1
pageSizeinteger20
POST /api/v1/media/upload

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

ConstraintValue
Max file size10 MB
Allowed MIME typesimage/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

StatusCodeDescription
400NO_FILENo file provided.
400FILE_TOO_LARGEFile exceeds 10MB limit.
400INVALID_TYPEFile type not allowed.
DELETE /api/v1/media/{id}

Delete a media file. Returns 204 No Content.

Management API — Layouts

Base path: /api/v1/layouts — Requires authentication.

GET /api/v1/layouts

List all layouts with pagination.

GET /api/v1/layouts/{id}

Get a layout with its components.

Response 200 OK

{
  "data": {
    "layout": { /* Layout object */ },
    "components": [ /* array of LayoutComponent objects */ ]
  }
}
POST /api/v1/layouts

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
}
PUT /api/v1/layouts/{id}

Update a layout. All fields optional (partial update).

DELETE /api/v1/layouts/{id}

Delete a layout. Cannot delete the default layout. Returns 204 No Content.

POST /api/v1/layouts/{id}/set-default

Set a layout as the default. The previous default is unset automatically.

Management API — Menus

Base path: /api/v1/menus — Requires authentication.

GET /api/v1/menus

List all navigation menus for the current site.

POST /api/v1/menus

Create a new navigation menu.

Request body

FieldTypeDescription
namestringDisplay name for the menu
slugstring?URL slug (auto-generated from name if omitted)
locationstring?header, footer, or custom. Defaults to header
isActivebool?Whether the menu is active. Defaults to true
GET /api/v1/menus/{id}

Get a menu with its items (flat list and rendered tree).

PUT /api/v1/menus/{id}

Update a menu’s name, location, or active status.

DELETE /api/v1/menus/{id}

Delete a menu and all its items.

POST /api/v1/menus/{id}/items

Add a new item to a menu.

Request body

FieldTypeDescription
labelstringDisplay text for the link
urlstring?URL for custom links
linkTypestring?custom, post, category, or tag
linkIdguid?Referenced post or category ID (when linkType is not custom)
targetstring?_self or _blank
parentIdguid?Parent item ID for nesting (dropdown)
sortOrderint?Display order
PUT /api/v1/menus/{id}/items/{itemId}

Update an existing menu item.

DELETE /api/v1/menus/{id}/items/{itemId}

Remove an item from a menu. Child items are re-parented to the deleted item’s parent.

PUT /api/v1/menus/{id}/reorder

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.

GET /api/v1/redirects

List all redirects for the current site with pagination metadata.

Query parameters

ParameterTypeDefaultDescription
pageinteger1Page number
pageSizeinteger50Items 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 /api/v1/redirects/{id}

Get a redirect by ID including hit count and last-hit timestamp.

POST /api/v1/redirects

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 */ }
}
PUT /api/v1/redirects/{id}

Update an existing redirect’s paths, status code, notes, or active state.

DELETE /api/v1/redirects/{id}

Permanently delete a redirect. Returns 204 No Content.

Management API — Plugins

Base path: /api/v1/plugins — Requires authentication.

GET /api/v1/plugins

List all installed plugins.

Query parameters

ParameterTypeDefaultDescription
enabledOnlyboolean—If true, only return enabled plugins
pageinteger1Page number
pageSizeinteger50Items per page
POST /api/v1/plugins/install

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 /api/v1/plugins/{id}

Get a single plugin by ID, including its settings and entry point code.

POST /api/v1/plugins/{id}/enable

Enable a disabled plugin.

POST /api/v1/plugins/{id}/disable

Disable an enabled plugin without uninstalling it.

PUT /api/v1/plugins/{id}/settings

Update a plugin's settings JSON.

Request body

{
  "settings": "{\"color\":\"#DC2626\",\"height\":\"4px\"}"
}

Response 200 OK

{
  "data": { "settings": "{\"color\":\"#DC2626\",\"height\":\"4px\"}" }
}
DELETE /api/v1/plugins/{id}

Uninstall and permanently remove a plugin. Returns 204 No Content.

Base path: /api/v1/search — Requires authentication.

GET /api/v1/search

Full-text search across all posts (including drafts). Requires the q parameter.

Query parameters

ParameterTypeDefaultDescription
qstringRequiredSearch query
pageinteger1Page number
pageSizeinteger20Items per page
GET /api/v1/search/suggestions

Get search autocomplete suggestions.

Query parameters

ParameterTypeDefault
qstringRequired
limitinteger5

Management API — Import/Export

Base path: /api/v1/import-export — Requires authentication.

POST /api/v1/import-export/wordpress

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

POST /api/v1/import-export/markdown

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

GET /api/v1/import-export/site

Export the entire site as JSON. Returns application/json.

GET /api/v1/import-export/posts/{id}/markdown

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...
GET /api/v1/import-export/posts/{id}/html

Export a single post as rendered HTML. Returns text/html.

Management API — Site

Base path: /api/v1/site — Requires authentication.

GET /api/v1/site

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.

GET /api/v1/themes

List all themes with pagination.

GET /api/v1/themes/{id}

Get a single theme by ID with full CSS variables.

POST /api/v1/themes

Create a new theme.

Request body

{
  "name": "Custom Theme",
  "slug": "custom-theme",
  "description": "A custom theme",
  "cssVariables": "{\"--font-body\":\"Inter\",\"--color-accent\":\"#3D5A80\"}"
}
PUT /api/v1/themes/{id}

Update a theme. All fields optional (partial update).

POST /api/v1/themes/{id}/activate

Set a theme as the active theme for the site.

Newsletter API

Base path: /api/v1/newsletter — Requires authentication (except subscriber signup).

GET /api/v1/newsletter/subscribers

List all subscribers with status filtering and pagination.

Query parameters

ParameterTypeDefaultDescription
statusstring—Filter: active, unsubscribed
pageinteger1Page number
pageSizeinteger50Items per page
POST /api/v1/newsletter/subscribe

Subscribe a new email address. Public endpoint (no auth required).

Request body

{
  "email": "reader@example.com"
}
POST /api/v1/newsletter/unsubscribe

Unsubscribe an email address. Public endpoint.

GET /api/v1/newsletter/campaigns

List all newsletter campaigns with status and pagination.

POST /api/v1/newsletter/campaigns

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.

POST /api/v1/membership/checkout

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

StatusCodeDescription
400INVALID_EMAILEmail is required.
400NOT_CONFIGUREDStripe is not configured.
POST /api/v1/membership/portal

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/..."
  }
}
GET /api/v1/membership/status

Check membership status for an email address. Public endpoint.

Query parameters

ParameterTypeDescription
emailstringEmail 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.

POST /api/v1/ai/complete

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

StatusCodeDescription
400AI_DISABLEDAI is not configured. Set up your API key in Settings > AI.
400AI_ERRORThe AI provider returned an error.
POST /api/v1/ai/stream

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]
POST /api/v1/ai/generate-theme

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"
}
POST /api/v1/ai/generate-layout

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 CategoryAPI EndpointsCapabilities
Content/api/v1/postsCreate, update, delete, publish, unpublish, schedule, preview, SEO analysis
Media/api/v1/mediaUpload, bulk upload, crop, update metadata, organize into folders
Categories/api/v1/categoriesCreate hierarchical categories, reorder, assign to posts
Navigation/api/v1/menusCreate menus, add/remove/reorder items, assign to locations
Search/api/v1/content/searchFull-text search, autocomplete suggestions (public, no auth)
AI/api/v1/aiText completion, streaming, theme generation, layout generation
Discovery/api/v1/contentRead published posts, categories, themes, layouts, menus (public)
Auth/api/v1/authToken exchange, refresh, user info
Interactive API Explorer: Use the Scalar API explorer at /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.

GET /api/v1/sites

List all sites in the installation.

POST /api/v1/sites

Create a new site.

Request body

{
  "name": "My Second Blog",
  "slug": "second-blog",
  "domain": "blog2.example.com"
}
GET /api/v1/sites/{id}

Get a single site by ID.

PUT /api/v1/sites/{id}

Update a site's configuration.

DELETE /api/v1/sites/{id}

Delete a site. Cannot delete the primary site. Returns 204 No Content.

POST /api/v1/sites/{id}/set-primary

Set a site as the primary site. The previous primary is unset automatically.

Scheduler API

Base path: /api/v1/scheduler — Requires authentication.

GET /api/v1/scheduler/tasks

List all scheduled tasks with their status and timing information.

POST /api/v1/scheduler/tasks

Create a new scheduled task.

Request body

{
  "name": "Clear Cache",
  "taskType": "cache_clear",
  "cronExpression": "0 * * * *",
  "isEnabled": true
}
GET /api/v1/scheduler/tasks/{id}

Get a single task with full details.

PUT /api/v1/scheduler/tasks/{id}

Update a scheduled task. All fields optional.

DELETE /api/v1/scheduler/tasks/{id}

Delete a scheduled task. Returns 204 No Content.

POST /api/v1/scheduler/tasks/{id}/trigger

Manually trigger a scheduled task to run immediately.

POST /api/v1/scheduler/run-due

Run all tasks that are due. Used by the background service.

Post Types API

Base path: /api/v1/post-types — Requires authentication.

GET /api/v1/post-types

List all custom post types.

POST /api/v1/post-types

Create a new post type.

Request body

{
  "name": "Recipe",
  "slug": "recipe",
  "icon": "chef-hat",
  "fieldSchema": "{\"ingredients\":\"array\",\"cookTime\":\"number\"}"
}
GET /api/v1/post-types/{id}

Get a single post type by ID.

PUT /api/v1/post-types/{id}

Update a post type. All fields optional.

DELETE /api/v1/post-types/{id}

Delete a post type. Returns 204 No Content.

Stripe Webhook

Endpoint: POST /api/v1/stripe/webhook — Called by Stripe. Signature verification required.

POST /api/v1/stripe/webhook

Receives Stripe webhook events. Verifies the Stripe-Signature header and processes events idempotently (using the StripeEvent table to prevent duplicate processing).

Handled events

EventAction
checkout.session.completedActivates membership for the subscriber
customer.subscription.updatedUpdates membership tier and expiry
customer.subscription.deletedDowngrades member to free tier
invoice.payment_failedTracks payment failures, downgrades after 3 strikes
Configuration: Set the Stripe:WebhookSecret environment variable to your Stripe webhook signing secret.

SEO & Feeds

Public endpoints for search engines and feed readers. No authentication required.

GET /sitemap.xml

Sitemap index with separate post and category sitemaps. Generated by the SEO service with priority and change frequency.

GET /feed.xml

RSS 2.0 feed of recent published posts.

GET /robots.txt

Auto-generated robots.txt with sitemap reference.

GET /llms.txt

Machine-readable site description for AI crawlers and LLMs.

Interactive API Explorer: All 135 endpoints are documented with request/response schemas in the Scalar API explorer at /scalar. Each endpoint includes EndpointSummary and EndpointDescription metadata for rich OpenAPI documentation.