↑ ↓ to navigate
↵ to select
esc to close
API Reference Advanced

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 URLUsed for
https://api.airogelcms.com/v1Account-independent endpoints: registration, plans, your user (/me) and the list of accounts you belong to
https://api.airogelcms.com/v1/accounts/:account_idEverything 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.com and 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_id in the URL. A request for an account you are not a member of returns 404.
  • 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/registration creates 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

MethodPathDescription
POST/v1/registrationCreate a user, an account and an API token in one call. No authentication. Rate-limited.
GET/v1/plansList the subscription plans you can check out
GET/v1/meThe authenticated user
DELETE/v1/mePermanently delete the authenticated user. This cannot be undone.
GET/v1/accountsList the accounts you are a member of
GET/v1/accounts/:idGet one account
POST/v1/accountsCreate 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.

PathMethodsReference
/collectionsCRUDCollections
/collections/:collection_id/entriesCRUDEntries
/blueprintsCRUDBlueprints
/assetsCRUDAssets
/templatesCRUDTemplates
/navigationsCRUDNavigations
/navigations/:navigation_id/itemsCRUDNavigations
/globalsCRUDGlobals
/formsCRUDForms
/contextsCRUDContexts (below)
/exportGETExport (below)
/subscription/checkoutPOSTSubscription (below)
/subscription/statusGETSubscription (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-Match concurrency control and dry_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.

PrefixResource
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 are collection, entry, blueprint, navigation, item (or navigation_item), global, form and context.
  • Wrapper required. Templates ({"template": {...}}), account creation ({"account": {...}}) and registration ({"registration": {...}}) must be wrapped. A flat body returns 400 Bad Request. The base64 JSON form of an asset upload is wrapped in asset.
  • Updates. PUT and PATCH behave 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 returns 422. A duplicate asset (same path and filename) returns 409 unless you pass replace.
  • 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-Match support. 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.

StatusMeaningBody
200 / 201Success / created (or upserted by handle)The resource
204DeletedEmpty
400A required wrapper key is missing (templates, accounts, registration)Minimal
401Missing, invalid or expired tokenEmpty
402The account has no active or trialing subscription{"error": "Active subscription required. ..."}
403API registration is currently disabled{"error": "API registration is currently disabled."}
404The resource does not exist, or you are not a member of the accountEmpty
409Conflict, for example an asset with the same path and filename already existsSee below
422Validation failed, or a referenced handle was not foundSee below
429Too 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:

  1. Templates. Upload layouts and templates first if collections or entries will reference them by template_handle/layout_handle. An unknown template handle returns 422.
  2. Blueprints. Define the field structures.
  3. Collections. Attach an existing blueprint with blueprint_handle. A blueprint_handle that does not exist yet is silently skipped. Entries in a collection with no blueprint then fail with 422 ("Collection has no blueprint attached"), so make sure step 2 succeeded.
  4. Assets. Upload images and files so entries can reference them.
  5. 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.
  6. Navigations (optional). Build menus. Items can link to the entries from step 5.
  7. 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/me returns {id, name, avatar_url, sgid, content} for the token's user.
  • GET /v1/accounts returns an array of {id, name, owner_id, account_users: [{user_id}], created_at, updated_at}. GET /v1/accounts/:id returns one of these objects.
  • POST /v1/accounts with {"account": {"name": "Second Site"}} creates an account owned by you and returns 201 with 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.

FieldTypeRequiredDescription
handlestringyessnake_case identifier, for example brand_voice
namestringyesDisplay name
textstringyesThe 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 data plus pagination. Field types use short names (rich_text, not BlueprintFieldRichText). Collections gained blueprint_handles, entries_count and feed fields. Globals gained blueprint_handle and fields. Forms gained notification_* and spam_check_enabled.
  • 2026-03-05: Added registration, plans, account creation and subscription checkout/status endpoints.
  • 2026-02-09: Added position for manual entry ordering.
  • 2026-01-10: Added collection index pages with pagination.
  • 2025-01-08: Initial release: core endpoints, forms and date-based routing.