REST API Overview
Base URLs, authentication, subscription requirement, route table, request and response conventions, pagination, errors and a quick-start walkthrough for the Airogel CMS REST API v1.
Overview
The Airogel CMS REST API (v1) is a JSON API for managing a site's content model, content, templates, media and structure from your own code: content importers, headless front ends, build scripts and other automation. Everything you can create through the API is also visible and editable in the dashboard at app.airogelcms.com.
If you are connecting an AI assistant rather than writing code, use the MCP server instead. It covers everything on this page plus several tools that have no REST equivalent. See MCP Integration.
Base URLs
| Base URL | Used for |
|---|---|
https://api.airogelcms.com/v1 | Account-independent endpoints: registration, plans, your user (/me) and the list of accounts you belong to |
https://api.airogelcms.com/v1/accounts/:account_id | Everything inside one account (one site): collections, entries, blueprints, assets, templates, navigations, globals, forms, contexts, export and subscription |
:account_id is the account's prefix ID, for example acct_V8pQx2nL4k. Call GET /v1/accounts to find it. There is no /api segment in the path. All requests and responses are JSON. Send Content-Type: application/json with JSON bodies. Asset uploads can use multipart/form-data instead.
Authentication
Send a bearer token in the Authorization header of every request (registration is the only exception):
Authorization: Bearer YOUR_API_TOKEN
- Creating a token: sign in at
app.airogelcms.comand go to Settings → API Tokens. Give the token a name and copy the value. You can revoke it from the same page. - Tokens belong to your user, not to an account. One token works for every account you are a member of. You pick the account with
:account_idin the URL. A request for an account you are not a member of returns404. - Expiry: tokens created in the dashboard do not expire. They stay valid until you revoke them.
- OAuth: OAuth 2.0 access tokens, the kind MCP clients get when you connect them, are accepted in the same header.
- Registration:
POST /v1/registrationcreates a user and an account and returns an API token once, in its response.
A missing, invalid, revoked or expired token returns 401 Unauthorized with an empty body.
Subscription requirement
Endpoints under /v1/accounts/:account_id/ need the account to have an active or trialing subscription. Any plan qualifies, and every plan includes API access. Without one, the request fails with 402 Payment Required:
{
"error": "Active subscription required. Please subscribe at <pricing page URL>"
}
These endpoints work without a subscription: /v1/registration, /v1/plans, /v1/me, /v1/accounts, /v1/accounts/:id and both /subscription endpoints. That lets a new account list plans, start checkout and poll its status before it has a subscription. See Billing for plan details.
Endpoints
Account-independent endpoints
| Method | Path | Description |
|---|---|---|
| POST | /v1/registration | Create a user, an account and an API token in one call. No authentication. Rate-limited. |
| GET | /v1/plans | List the subscription plans you can check out |
| GET | /v1/me | The authenticated user |
| DELETE | /v1/me | Permanently delete the authenticated user. This cannot be undone. |
| GET | /v1/accounts | List the accounts you are a member of |
| GET | /v1/accounts/:id | Get one account |
| POST | /v1/accounts | Create a new account that you own. It starts with default site content. |
Account endpoints
Every path below is relative to /v1/accounts/:account_id. Resources marked "CRUD" support GET (list), POST (create), GET /:id, PUT/PATCH /:id and DELETE /:id.
| Path | Methods | Reference |
|---|---|---|
/collections | CRUD | Collections |
/collections/:collection_id/entries | CRUD | Entries |
/blueprints | CRUD | Blueprints |
/assets | CRUD | Assets |
/templates | CRUD | Templates |
/navigations | CRUD | Navigations |
/navigations/:navigation_id/items | CRUD | Navigations |
/globals | CRUD | Globals |
/forms | CRUD | Forms |
/contexts | CRUD | Contexts (below) |
/export | GET | Export (below) |
/subscription/checkout | POST | Subscription (below) |
/subscription/status | GET | Subscription (below) |
Available through MCP only
These features have no REST endpoint. Use them through the MCP server or the dashboard:
- Custom domains and domain verification (see Custom Domains)
- Redirect rules
- Events and event tickets (see Events)
- Constituents (see Constituents)
- Site search (see Full-Text Search)
- Structured entry queries, relation queries and single-field patches on entries
- Upload from a URL, chunked or direct uploads, and browser upload sessions for large files
- Theme validation, Liquid docs generation and page preview rendering
- ETag /
If-Matchconcurrency control anddry_run - Collection feed settings, entry
routing_override, asset descriptive metadata (alt, caption, credit, license) and form notification settings. REST responses include these values but cannot change them.
Requests
Resource IDs
Wherever a path has an :id (or :collection_id, :navigation_id), you can pass either the resource's prefix ID or its handle:
GET /v1/accounts/acct_V8pQx2nL4k/collections/clctn_3kLm9pQr
GET /v1/accounts/acct_V8pQx2nL4k/collections/posts
There are two exceptions. Assets and navigation items have no handle, so they are addressed by prefix ID only. :account_id is always the account's prefix ID.
| Prefix | Resource |
|---|---|
acct_ | Account |
clctn_ | Collection |
cnety_ | Entry |
blprt_ | Blueprint |
actast_ | Asset |
tmplt_ | Template |
nav_ | Navigation |
ni_ | Navigation item |
stgbl_ | Global |
form_ | Form |
actx_ | Context |
plan_ | Plan |
user_ | User |
Request bodies
- Flat or wrapped. Most resources accept attributes at the top level of the body (
{"name": "Blog", ...}) or wrapped in the resource name ({"collection": {"name": "Blog", ...}}). The accepted wrapper keys arecollection,entry,blueprint,navigation,item(ornavigation_item),global,formandcontext. - Wrapper required. Templates (
{"template": {...}}), account creation ({"account": {...}}) and registration ({"registration": {...}}) must be wrapped. A flat body returns400 Bad Request. The base64 JSON form of an asset upload is wrapped inasset. - Updates.
PUTandPATCHbehave the same way. Both are partial updates: attributes you leave out stay as they are. - POST is idempotent by handle for collections, blueprints, globals, navigations, templates, entries and contexts. If you POST a handle that already exists, that record is updated in place instead of duplicated, and the response is still
201. This makes re-running an import safe. Navigation items are matched by title plus URL (or title plus linked entry). Forms and assets are plain creates. A duplicate form handle returns422. A duplicate asset (same path and filename) returns409unless you passreplace. - Unknown keys are ignored. Content values on entries and globals are read only for keys that match a field handle on the blueprint. Any other key is dropped silently.
- Last write wins. REST has no ETag or
If-Matchsupport. If you need optimistic locking, use the MCP tools.
Pagination
Every list endpoint under an account is paginated and returns a data array plus a pagination object:
{
"data": [ ... ],
"pagination": {
"page": 2,
"per_page": 20,
"total": 47,
"total_pages": 3,
"next_page": 3,
"prev_page": 1
}
}
Pass ?page=N to choose a page. It starts at 1. The page size is fixed at 20 and cannot be changed. next_page is null on the last page and prev_page is null on the first. Loop until next_page is null.
Two account-independent lists are not paginated. GET /v1/accounts returns a bare JSON array, and GET /v1/plans returns {"plans": [...]}.
Responses and errors
Single resources come back as a bare object, for example {"id": "clctn_...", "handle": "posts", ...}. There are two exceptions: templates are wrapped in {"template": {...}} and assets in {"asset": {...}}. Timestamps are ISO 8601 in UTC. DELETE returns 204 No Content.
| Status | Meaning | Body |
|---|---|---|
| 200 / 201 | Success / created (or upserted by handle) | The resource |
| 204 | Deleted | Empty |
| 400 | A required wrapper key is missing (templates, accounts, registration) | Minimal |
| 401 | Missing, invalid or expired token | Empty |
| 402 | The account has no active or trialing subscription | {"error": "Active subscription required. ..."} |
| 403 | API registration is currently disabled | {"error": "API registration is currently disabled."} |
| 404 | The resource does not exist, or you are not a member of the account | Empty |
| 409 | Conflict, for example an asset with the same path and filename already exists | See below |
| 422 | Validation failed, or a referenced handle was not found | See below |
| 429 | Too many registration attempts | {"error": "Too many registration attempts. Try again later."} |
Validation errors
Collections, entries, blueprints, navigations, navigation items, globals, forms and contexts all use one error shape. details is always an array of {path, message} objects:
{
"error": "Validation failed",
"details": [
{ "path": "handle", "message": "has already been taken" },
{ "path": "fields[1].type", "message": "\"richtext\" is not a field type; expected one of collection, dictionary, entity, enumerate, gallery, image, list, number, raw_html, rich_text, template, text, toggle, video" }
]
}
For nested input, path is dotted and indexed, for example blueprint.handle or fields[1].type. The same shape is used when a reference cannot be resolved, such as an unknown template_handle, layout_handle, blueprint_handle, entry_handle or parent_id. It is also used for 409 conflicts from these resources.
Endpoints with a different error shape
- Templates, assets, account creation and registration return a flat list of messages:
{"errors": ["Liquid syntax error: ..."]}. - Asset conflicts (409) return the existing asset so you can reuse it:
{"error": "An asset with this filename already exists at this path", "created": false, "asset": {...}}. - Subscription endpoints return
{"error": "Plan not found."}(404) or{"error": "..."}(422).
Rate limits
Authenticated endpoints have no rate limits. POST /v1/registration is limited to 10 requests per 3 minutes per client. Requests over that limit get 429. Please keep request volume reasonable, for example by running large imports sequentially rather than in wide parallel bursts.
Import order of operations
When you import content from another system, such as WordPress, create things in dependency order:
- Templates. Upload layouts and templates first if collections or entries will reference them by
template_handle/layout_handle. An unknown template handle returns422. - Blueprints. Define the field structures.
- Collections. Attach an existing blueprint with
blueprint_handle. Ablueprint_handlethat does not exist yet is silently skipped. Entries in a collection with no blueprint then fail with422("Collection has no blueprint attached"), so make sure step 2 succeeded. - Assets. Upload images and files so entries can reference them.
- Entries. Create content. Image fields reference assets by
actast_ID or by path. Entity fields reference entries that already exist, so create referenced entries first. - Navigations (optional). Build menus. Items can link to the entries from step 5.
- Globals (optional). Add site-wide settings.
Because POST upserts by handle, you can safely re-run an interrupted import from the beginning.
Quick start
This walkthrough imports one blog post. Set two shell variables first:
export API_TOKEN="your-token-from-settings"
export BASE="https://api.airogelcms.com/v1"
1. Find your account ID
curl -H "Authorization: Bearer $API_TOKEN" "$BASE/accounts"
[
{
"id": "acct_V8pQx2nL4k",
"name": "My Site",
"owner_id": "user_7hQm2xPz",
"account_users": [ { "user_id": "user_7hQm2xPz" } ],
"created_at": "2026-09-01T14:02:11Z",
"updated_at": "2026-09-01T14:02:11Z"
}
]
export ACCOUNT_ID="acct_V8pQx2nL4k"
2. Create a blueprint
curl -X POST "$BASE/accounts/$ACCOUNT_ID/blueprints" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Blog Post",
"handle": "blog_post",
"fields": [
{"handle": "body", "display": "Body", "type": "rich_text"},
{"handle": "excerpt", "display": "Excerpt", "type": "text"},
{"handle": "featured_image", "display": "Featured Image", "type": "image"}
]
}'
3. Create a collection that uses it
curl -X POST "$BASE/accounts/$ACCOUNT_ID/collections" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Blog Posts",
"handle": "posts",
"routing": "blog/:handle",
"orderable": "descending",
"blueprint_handle": "blog_post"
}'
4. Upload an image
curl -X POST "$BASE/accounts/$ACCOUNT_ID/assets" \
-H "Authorization: Bearer $API_TOKEN" \
-F "file=@photo.jpg" \
-F "path=uploads/2026/09"
5. Create an entry
curl -X POST "$BASE/accounts/$ACCOUNT_ID/collections/posts/entries" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"handle": "hello-world",
"title": "Hello World",
"published": true,
"published_at": "2026-09-15T10:00:00Z",
"body": "<p>Our first post.</p>",
"excerpt": "Our first post",
"featured_image": "uploads/2026/09/photo.jpg"
}'
The response includes "content_path": "/blog/hello-world", which is the URL the entry is served at once a template is assigned. See Routing and Liquid Templates.
6. List what you created
curl -H "Authorization: Bearer $API_TOKEN" \
"$BASE/accounts/$ACCOUNT_ID/collections/posts/entries?published=true&page=1"
Other endpoints
Registration
POST /v1/registration needs no authentication. It creates a user, an account owned by that user, and an API token. The body must be wrapped in registration:
curl -X POST "$BASE/registration" \
-H "Content-Type: application/json" \
-d '{
"registration": {
"name": "Ada Lovelace",
"email_address": "ada@example.com",
"password": "a-long-secure-password",
"account_name": "Ada's Site",
"terms_of_service": true
}
}'
{
"user": { "id": "user_7hQm2xPz", "name": "Ada Lovelace", "email_address": "ada@example.com", "created_at": "2026-09-25T09:00:00Z" },
"account": { "id": "acct_V8pQx2nL4k", "name": "Ada's Site", "subdomain": "adas-site", "created_at": "2026-09-25T09:00:00Z" },
"api_token": "the-new-token"
}
The token appears only in this response, so store it. Errors return 422 {"errors": [...]}, for example when the email address is already taken. The new account has no subscription yet, so start checkout (below) before calling content endpoints.
Accounts and your user
GET /v1/mereturns{id, name, avatar_url, sgid, content}for the token's user.GET /v1/accountsreturns an array of{id, name, owner_id, account_users: [{user_id}], created_at, updated_at}.GET /v1/accounts/:idreturns one of these objects.POST /v1/accountswith{"account": {"name": "Second Site"}}creates an account owned by you and returns201with the account object. The account comes with default site content. It needs its own subscription before its content endpoints work.
Plans and subscription
GET /v1/plans lists the plans you can subscribe to:
{
"plans": [
{
"id": "plan_Qm8x2Lp",
"name": "Basic",
"tier": "basic",
"amount": "19.00",
"amount_cents": 1900,
"currency": "usd",
"interval": "month",
"trial_period_days": 14,
"charge_per_unit": false,
"unit_label": null,
"has_setup_fee": false,
"setup_fee_cents": 0,
"features": ["..."]
}
]
}
POST /v1/accounts/:account_id/subscription/checkout with {"plan": "plan_Qm8x2Lp"} returns {"checkout_url": "https://checkout.stripe.com/..."}. You can also pass success_url and cancel_url. Open the URL in a browser to pay. An unknown plan returns 404 {"error": "Plan not found."}.
Activation happens in the background after payment. Poll GET /v1/accounts/:account_id/subscription/status until subscribed is true:
{
"subscribed": true,
"plan": { "id": "plan_Qm8x2Lp", "name": "Basic", "tier": "basic", "interval": "month", "amount_cents": 1900, "currency": "usd" },
"subscription": { "id": "...", "status": "trialing", "trial_ends_at": "2026-10-09T09:00:00Z", "current_period_end": "2026-10-09T09:00:00Z", "cancel_at_period_end": false }
}
Before the account subscribes, the response is {"subscribed": false, "plan": null, "subscription": null}.
Contexts
Contexts are short named notes attached to an account, such as site_purpose, brand_voice or content_guidelines. AI assistants read them through MCP to learn about a site before they edit it. /v1/accounts/:account_id/contexts supports the full set of CRUD operations. Items can be addressed by actx_ ID or handle. POST upserts by handle.
| Field | Type | Required | Description |
|---|---|---|---|
handle | string | yes | snake_case identifier, for example brand_voice |
name | string | yes | Display name |
text | string | yes | The note itself |
curl -X POST "$BASE/accounts/$ACCOUNT_ID/contexts" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"handle": "brand_voice", "name": "Brand Voice", "text": "Friendly, plain-spoken, no jargon."}'
{
"id": "actx_4nRt8wQ",
"handle": "brand_voice",
"name": "Brand Voice",
"text": "Friendly, plain-spoken, no jargon.",
"created_at": "2026-09-25T09:10:00Z",
"updated_at": "2026-09-25T09:10:00Z"
}
Export
GET /v1/accounts/:account_id/export returns the account's published content as one JSON document. It includes content paths, globals, navigations, forms and every collection's entries. Use /export.yaml to download the same data as a YAML file named <account-name>-export.yaml. Add ?include_unpublished=true to include unpublished entries.
curl -H "Authorization: Bearer $API_TOKEN" \
-o site-export.yaml \
"$BASE/accounts/$ACCOUNT_ID/export.yaml?include_unpublished=true"
Changelog
See the changelog for the full history. Recent API changes:
- 2026-03-12: Response shapes unified. Every list returns
datapluspagination. Field types use short names (rich_text, notBlueprintFieldRichText). Collections gainedblueprint_handles,entries_countand feed fields. Globals gainedblueprint_handleandfields. Forms gainednotification_*andspam_check_enabled. - 2026-03-05: Added registration, plans, account creation and subscription checkout/status endpoints.
- 2026-02-09: Added
positionfor manual entry ordering. - 2026-01-10: Added collection index pages with pagination.
- 2025-01-08: Initial release: core endpoints, forms and date-based routing.