REST API
Drive your help center from your own systems: sync articles from a CMS, publish from CI, pull your product knowledge into another tool, or queue walkthrough generation from a script. Everything under /v1 is a stable, versioned contract — new fields and endpoints may be added, existing ones do not change meaning.
Full endpoint reference
Every endpoint with its required scope, parameters, request and response schemas, examples and error codes — generated from the API itself, so it always matches what the API returns.
Authentication
Every request carries a workspace API key as a bearer token. Create one in Settings → API keys. The key is shown once, at creation: we store only a hash of it, so it cannot be recovered later — if it is lost, revoke it and mint another. Only a workspace owner can manage keys.
curl https://api.macroprimer.com/v1/me \
-H "Authorization: Bearer mp_live_xxxxxxxxxxxxxxxxxxxx"- A key addresses exactly one workspace — its own. No parameter selects a tenant.
- A key can never mint, modify or revoke keys. That requires a signed-in owner.
- Keys do not open the dashboard or admin surfaces.
- Keep keys server-side. A key in browser or mobile code is public. Use one key per system, named for that system, so revoking one never breaks the others.
Your first request
GET /v1/me confirms the key works, names the workspace it addresses, and lists the scopes it holds. It needs no scope — so it is both the right first call and the fastest way to diagnose an unexpected 403.
{
"api_key": {
"name": "CI publishing",
"prefix": "mp_live_a1b2c3d4",
"scopes": ["articles:read", "articles:write"],
"expires_at": "2026-11-17T00:00:00+00:00",
"request_count": 1043
},
"workspace": { "id": "c1…", "name": "Acme", "slug": "acme" }
}Permissions
Scopes are resource:action. Read and write are independent — articles:write does not grant articles:read. Grant exactly what an integration needs. Wildcards (articles:*, *) are accepted but widen silently as new actions ship, so prefer explicit scopes.
| articles:read | Read articles, bodies and revision history. |
| articles:write | Create, update, publish, delete and restore articles. |
| categories:read | Read the collection tree. |
| categories:write | Create, update, reorder and delete collections. |
| comments:read | Read editorial comments and findings. |
| comments:write | Post, resolve and delete comments. |
| search:read | Search the knowledge base. |
| translations:write | Machine-translate articles and collections. |
| knowledge:read | Read the product map, items, surfaces, search, export and audit. |
| knowledge:write | Start knowledge builds and suggestion runs; set the product profile. |
| knowledge:ingest | Upload what the MacroPrimer GitHub Action derives from a repository, in place of repository access. |
| source:read | See the repository file paths behind knowledge items and surfaces. |
| walkthroughs:read | List walkthrough jobs and coverage. |
| walkthroughs:write | Enqueue and retry walkthrough generation. |
| assets:read | List and read assets. |
| assets:write | Upload and delete assets. |
| workspace:read | Read settings, branding, languages, plan and stats. |
| workspace:write | Update settings, branding and languages. |
Conventions
Pagination. List endpoints take limit (1–100, default 25) and offset, and wrap results in a uniform envelope. Page until has_more is false.
{
"data": [ … ],
"pagination": { "limit": 25, "offset": 0, "total": 214, "has_more": true }
}Partial updates. PATCH changes only the fields you send; omitted fields keep their value.
Errors. One envelope everywhere. Branch on error.type, never on the message, which may be reworded.
{ "error": { "type": "forbidden", "status": 403,
"message": "this API key is missing the required scope: articles:write" } }| 401 | unauthorized | Missing, unknown, revoked or expired key. |
| 402 | payment_required | Plan limit reached. X-Upgrade-Plan names the plan that lifts it. |
| 403 | forbidden | Valid key, missing scope. X-Required-Scope names the one needed. |
| 404 | not_found | No such resource in this workspace. |
| 409 | conflict | E.g. that language variant already exists. |
| 413 | payload_too_large | Upload over the size limit. |
| 422 | invalid_request | Validation failed. Includes a fields array of {field, reason}. |
| 429 | rate_limited | Over your plan's per-minute budget. Retry-After says how long to wait. |
| 503 | service_unavailable | A prerequisite is missing, e.g. no AI credential configured. |
Rate limits. Enforced per workspace — not per key, so extra keys do not buy extra budget — in a fixed 60-second window sized by your plan: 60/min on Free, 300/min on Pro, 1,200/min on Business. Every response reports where you stand, so a client can pace itself rather than discover the limit by being refused.
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 274
X-RateLimit-Reset: 1787506596 # unix seconds, when the window resetsOver the limit returns 429 with Retry-After in seconds. Honour that value instead of guessing a backoff — it is exactly the time left in the window. Refused requests still count, so hammering while limited keeps the window full.
Content model
group_id is the logical document. An article record is one language variant; every translation of it shares a group_id. Sync against group_id, not id. Collections work the same way.
The default language is the source of truth. It defines structure, ordering and the search corpus; other languages overlay text onto it, and anything untranslated falls back — so a locale never shows dead links. Editing the source marks its translations stale, which is your signal to re-translate.
Endpoints
58 operations. The scope each one requires is listed beside it.
Meta
| GET | /v1/me | any key |
| GET | /v1/scopes | any key |
Articles
List filters: language, collection_id, status, group_id, q, updated_since, include_body.
| GET | /v1/articles | articles:read |
| POST | /v1/articles | articles:write |
| GET | /v1/articles/{id} | articles:read |
| PATCH | /v1/articles/{id} | articles:write |
| DELETE | /v1/articles/{id} | articles:write |
| POST | /v1/articles/{id}/publish | articles:write |
| POST | /v1/articles/{id}/unpublish | articles:write |
| GET | /v1/articles/{id}/revisions | articles:read |
| GET | /v1/articles/{id}/revisions/{rev_id} | articles:read |
| POST | /v1/articles/{id}/revisions/{rev_id}/restore | articles:write |
Collections
| GET | /v1/collections | categories:read |
| GET | /v1/collections/tree | categories:read |
| POST | /v1/collections | categories:write |
| GET | /v1/collections/{id} | categories:read |
| PATCH | /v1/collections/{id} | categories:write |
| POST | /v1/collections/reorder | categories:write |
| DELETE | /v1/collections/{id} | categories:write |
Comments
Comments attach to an article's group_id, so they are shared across translations.
| GET | /v1/articles/{group_id}/comments | comments:read |
| POST | /v1/articles/{group_id}/comments | comments:write |
| PATCH | /v1/comments/{id} | comments:write |
| DELETE | /v1/comments/{id} | comments:write |
Search
| GET | /v1/search | search:read |
Translations
Synchronous — one model call per target language. Requires an AI credential on the workspace.
| GET | /v1/translations/status | workspace:read |
| POST | /v1/articles/{id}/translate | translations:write |
| POST | /v1/collections/{id}/translate | translations:write |
Knowledge
Jobs are asynchronous: start, then poll. Starting a kind already running returns the running job.
| GET | /v1/knowledge/map | knowledge:read |
| GET | /v1/knowledge/status | knowledge:read |
| GET | /v1/knowledge/jobs | knowledge:read |
| POST | /v1/knowledge/jobs | knowledge:write |
| GET | /v1/knowledge/jobs/{id} | knowledge:read |
| POST | /v1/knowledge/jobs/{id}/cancel | knowledge:write |
| GET | /v1/knowledge/suggestions | knowledge:read |
| GET | /v1/knowledge/suggestions/{id} | knowledge:read |
| POST | /v1/knowledge/suggestions/{id}/dismiss | knowledge:write |
| GET | /v1/knowledge/overview | knowledge:read |
| GET | /v1/knowledge/items | knowledge:read |
| GET | /v1/knowledge/items/{key} | knowledge:read |
| GET | /v1/knowledge/surfaces | knowledge:read |
| GET | /v1/knowledge/surfaces/{key} | knowledge:read |
| GET | /v1/knowledge/search | knowledge:read |
| GET | /v1/knowledge/export | knowledge:read |
| GET | /v1/knowledge/audit | knowledge:read |
| GET | /v1/knowledge/profile | knowledge:read |
| PUT | /v1/knowledge/profile | knowledge:write |
| DELETE | /v1/knowledge/profile | knowledge:write |
| POST | /v1/knowledge/action/session | knowledge:ingest |
| POST | /v1/knowledge/action/token | knowledge:ingest |
| PUT | /v1/knowledge/action/bundle | knowledge:ingest |
MCP
The same knowledge as read-only tools for AI agents, over the Model Context Protocol (Streamable HTTP). Tools: get_overview, search_knowledge, get_item, list_items, list_surfaces, get_surface, lookup_term. Repository paths need source:read.
| POST | /mcp | knowledge:read |
Walkthroughs
Metered against your plan's monthly quota. Over-quota is reported in the response, not raised as an error.
| GET | /v1/walkthroughs | walkthroughs:read |
| GET | /v1/walkthroughs/coverage | walkthroughs:read |
| GET | /v1/walkthroughs/{id} | walkthroughs:read |
| POST | /v1/walkthroughs | walkthroughs:write |
| POST | /v1/walkthroughs/retry | walkthroughs:write |
Assets
Upload sends raw bytes with the format in Content-Type (PNG, JPEG, SVG, WEBP, GIF; max 5 MB).
| GET | /v1/assets | assets:read |
| GET | /v1/assets/{id} | assets:read |
| POST | /v1/assets | assets:write |
| DELETE | /v1/assets/{id} | assets:write |
Workspace
| GET | /v1/workspace | workspace:read |
| PATCH | /v1/workspace | workspace:write |
| GET | /v1/workspace/branding | workspace:read |
| PATCH | /v1/workspace/branding | workspace:write |
| GET | /v1/workspace/languages | workspace:read |
| PATCH | /v1/workspace/languages | workspace:write |
| GET | /v1/workspace/stats | workspace:read |
Recipes
Publish from CI.
curl -X POST https://api.macroprimer.com/v1/articles \
-H "Authorization: Bearer $MACROPRIMER_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Release 4.2","body":"<p>…</p>","status":"published"}'Incremental sync. Poll updated_since with the timestamp of your last successful run instead of re-listing everything.
curl -sG https://api.macroprimer.com/v1/articles \
-H "Authorization: Bearer $MACROPRIMER_KEY" \
--data-urlencode "updated_since=2026-08-01T00:00:00Z" \
--data-urlencode "include_body=true" \
--data-urlencode "limit=100"Re-translate what has gone stale.
curl -sG https://api.macroprimer.com/v1/articles -H "Authorization: Bearer $KEY" \
--data-urlencode "limit=100" \
| jq -r '.data[] | select(.translation.stale) | .id' \
| while read id; do
curl -sX POST "https://api.macroprimer.com/v1/articles/$id/translate" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"target_langs":["es","fr"]}'
done