Skip to content

Content

Send the site's pages as Markdown, and Manifest builds a documentation site from them.


Send content

If you have the content but no site, send it in one JSON request: the site's pages, written in Markdown, plus site settings and theme. Manifest builds them into a documentation site from its docs template. There is no build step on your side and no zip. Each ingest is a new deployment of the whole site, so it replaces everything the previous ingest published.

Content works on projects your keys created through the API. On your other projects it answers 422 not_api_project, so it never replaces a site you built another way; publish a zip to those instead.

In the request, each page is an entry in articles, with a title, a slug for its URL, the group it is listed under in the navigation, and its markdown.

curl -X POST https://api.manifestx.dev/v1/projects/proj_…/content \
  -H "Authorization: Bearer $MANIFEST_ACCOUNT_KEY" \
  -H "Idempotency-Key: help-center-sync-0193" \
  -H "Content-Type: application/json" \
  -d @content.json
{
  "template": "docs",
  "env": "staging",
  "site": {
    "name": "Acme Help",
    "description": "Answers for Acme customers.",
    "theme": {
      "tokens": { "color-brand-surface": "oklch(55% 0.2 260)" },
      "dark": { "color-brand-surface": "#6677ff" }
    }
  },
  "groups": [{ "slug": "start", "description": "First steps with Acme." }],
  "articles": [
    { "group": "Getting started", "group_slug": "start", "title": "Welcome", "slug": "welcome", "markdown": "Hello from Acme.", "order": 1 },
    { "group": "Billing", "title": "Invoices", "slug": "invoices", "markdown": "## Download\n\nUse the portal.", "icon": "lucide:receipt" }
  ]
}
Field Type Required Notes
template string yes docs.
env string no staging (default) or production.
site.name string yes Page title, header, and app name. Up to 120 characters.
site.description string no Meta description and the landing-page intro. Up to 1,000 characters.
site.theme object no tokens (light) and dark: theme token names mapped to CSS values. See below.
groups array no {slug, description?} per group. Listed groups come first, in this order, with their description.
articles array yes At least one page.

Each entry in articles is one page:

Field Type Required Notes
group string yes Group name shown in the navigation.
group_slug string no URL segment of the group. Defaults to the group name in lowercase with hyphens.
title string yes Up to 200 characters.
slug string yes URL segment of the page. See the slug rules below.
markdown string yes The page body. If its first line isn't a # heading, the title is added as one.
icon string no An Iconify name such as lucide:receipt. Defaults to lucide:file-text.
order number no Sort position inside the group. Pages without one follow, in the order you sent them.

Pages carry markdown only: a field not listed here is refused, in a page, site, site.theme, or groups, and so is markdown containing control characters. Images and other files are not part of the payload, so link to them by absolute URL.

The site answers 201 Created and is live on that environment's URL straight away:

{
  "deployment": { "id": "dep_…", "env": "staging", "status": "ready", "files": 10, "bytes": 26676 },
  "template": { "name": "docs", "version": "0.1.2" },
  "url": "https://acme-docs-staging.manifestx.ai",
  "request_id": "…"
}

Rarely, the site goes live but a follow-up step, such as setting up the default hostname, doesn't finish. The answer is still 201, with a warning:

{ "warning": { "code": "post_publish_incomplete", "message": "The site is live, but a follow-up step …" } }

The deployment is live and counts as published, so don't resend it. The step completes on the next publish, and url can be null until then.

A page is served at /docs/<group_slug>/<slug>. Promote, rollback, and the deployment list work exactly as for your own builds.


Template version

A project's first successful ingest pins the docs template version it was built with, and every later ingest uses the same version, so a template update never changes a live site unasked. The pinned version is in every response.


Slug rules

Slugs are lowercase letters and digits in words joined by single hyphens, such as payment-methods, up to 80 characters. Because the site matches a link against every part of its path:

  • a page slug is unique across all groups, not only within its own;
  • a page slug can't equal any group slug: a billing page inside a billing group is refused, so name it billing-overview;
  • neither can be docs, the segment the site lives under;
  • one group_slug has one group name, and two groups can't share a name.

Theme values

Token names are the template's theme tokens, with or without the leading --. Values can be colors, numbers, lengths, quoted font names, and these functions: rgb, rgba, hsl, hsla, hwb, lab, lch, oklab, oklch, color, color-mix, var, calc, min, max, clamp, light-dark. url(), comments, semicolons, braces, unclosed quotes or parentheses, and values over 200 characters are refused. Up to 100 tokens each for light and dark.


Content limits

Limit Value
Request body 12 MB
Pages per ingest 1,000
Markdown per page 512 KB
Markdown per ingest, in total 10 MB
Entries in groups 100
Slug and group_slug 80 characters, including a group_slug derived from the group name
Group name, page title 200 characters
Group description 1,000 characters
Site name 120 characters
Site description 1,000 characters
Theme tokens 100 each for light and dark
Theme value 200 characters

Retrying an ingest

Send an Idempotency-Key with every ingest. A retry with the same key and body returns the first response and creates no second deployment; the same key with a different body is 422 idempotency_key_reused. A 5xx is never stored, so a retry after one runs again.


Errors

A content problem answers 422 with a field that names the page and property, and no deployment is created:

{
  "error": "invalid_content",
  "message": "articles[1].slug: invalid slug \"Bad Slug\".",
  "field": "articles[1].slug",
  "request_id": "…"
}
Status error Meaning
400 invalid_input The body isn't UTF-8 JSON, isn't an object, has an unknown top-level field, template isn't docs, or env is wrong.
404 not_found No such project, or the key can't reach it.
413 payload_too_large The body is over 12 MB.
422 invalid_content A slug, size, theme value, or page field is invalid. field says which.
422 not_api_project The project wasn't created through the API. Publish a zip to it instead.
429 publish_limit_reached A Free project's daily publish limit is reached. Wait the seconds in Retry-After.
500 ingest_failed The deployment failed before going live. Whatever was live on that environment is unchanged (or nothing, on a first ingest). Retry the request.
503 template_unavailable The docs template couldn't be loaded. Retry shortly.