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.
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: features, use cases, glossary. |
| knowledge:write | Start knowledge builds and suggestion runs. |
| 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 |
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