https://api-staging.sappo.xco.agency
The Theme Maestro API lets you read your own store's Theme Maestro data from your own code: a spreadsheet sync, an internal dashboard, a warehouse system, or an AI assistant.
It is a JSON API over HTTPS. Every request is authenticated with a token you create yourself inside the app, and every request only ever sees the store that token belongs to — there is no way to ask for another store's data, and no store id to pass.
https://api-staging.sappo.xco.agencyGET https://api-staging.sappo.xco.agency/theme/v1/{resource}Authorization: Bearer mak_…Responses are always JSON. A successful resource response carries a data
array and a meta object; a failed one carries a single error object and
nothing else.
No endpoint changes your data: everything here is a read.
Open the app in your Shopify admin, go to Settings → API Access, and create a token. The full token is shown once, at creation time; afterwards only its first 12 characters are stored in readable form. If that card is locked, API access is not included in your current plan and the app will not create a token until the plan changes. At most three tokens can be active at once, so revoke one you no longer use before creating another.
A token can be given an optional expiry date, and can be revoked at any time from the same screen. Revoking takes effect on the next request.
Put it in the Authorization header of every request, after the word
Bearer:
curl "https://api-staging.sappo.xco.agency/theme/v1/scheduled-themes" \
-H "Authorization: Bearer mak_your_token_here"
There is no other accepted form: a query-string token, a cookie, or a
different header name is rejected with MISSING_TOKEN.
The token carries everything this API can do with your whole store's data in this app, so treat it like a password:
A token is stored only as a SHA-256 hash, so nobody — including us — can read it back to you. If you lose it, create a new one and revoke the old.
Every resource endpoint is a GET on a resource list:
GET https://api-staging.sappo.xco.agency/theme/v1/{resource}
{resource} is one of the names listed under Resources below. The v1
segment is the API version (see Versioning).
Ask for JSON and read JSON back:
curl "https://api-staging.sappo.xco.agency/theme/v1/scheduled-themes?page=1&page_size=50" \
-H "Authorization: Bearer mak_your_token_here" \
-H "Accept: application/json"
A successful response is always this shape:
{
"data": [ { "id": 1 } ],
"meta": { "page": 1, "pageSize": 50, "total": 137 }
}
data holds the records for the page you asked for, in the resource's own
field names. meta describes the page.
Each resource's example response below is abridged: a record carries every column stored for it, so expect fields the example does not show. Some resources return customer details, email addresses or codes among them — read the resource's own description before you log, cache or forward a response.
These documentation endpoints need no token at all, so you can explore before you create one:
| Endpoint | What it returns |
|---|---|
GET https://api-staging.sappo.xco.agency/theme/docs |
This page |
GET https://api-staging.sappo.xco.agency/theme/docs.json |
OpenAPI 3.0 document |
GET https://api-staging.sappo.xco.agency/theme/docs/content.json |
This documentation as JSON |
GET https://api-staging.sappo.xco.agency/theme/docs/mcp |
The MCP guide |
GET https://api-staging.sappo.xco.agency/theme/docs/mcp.json |
The MCP guide as JSON |
Lists are paged with two query parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page |
integer | 1 |
1-based. Anything below 1 is treated as 1. |
page_size |
integer | 25 |
Capped at 100. A larger value is silently reduced to 100. |
Note the asymmetry: the request parameter is page_size (snake case), while
the field echoed back in meta is pageSize (camel case).
"meta": { "page": 2, "pageSize": 25, "total": 137 }
total is the number of records matching your filters, not the number on
this page. Walk the list until page * pageSize >= total:
# 137 records at 25 per page = 6 pages
for page in 1 2 3 4 5 6; do
curl -s "https://api-staging.sappo.xco.agency/theme/v1/scheduled-themes?page=$page&page_size=25" \
-H "Authorization: Bearer mak_your_token_here"
done
Records are returned newest first. Which timestamp "newest" means depends on the resource — creation time for most, last-change or scheduled time for a few — and each resource's description below says which. Because new records can arrive while you page, a long walk can show a record twice or skip one; if that matters, page from the last page backwards, or filter to a window that has stopped changing.
Any query parameter other than page and page_size is passed to the
resource as a filter. Each resource accepts its own set — the supported
names, types and allowed values are listed per resource under Resources
below, and in the OpenAPI document.
# Only the records a resource's own `status` filter matches
curl "https://api-staging.sappo.xco.agency/theme/v1/scheduled-themes?status=PENDING" \
-H "Authorization: Bearer mak_your_token_here"
Three things to know:
200, so check your spelling against the
resource's parameter list if a result looks too large.true, 1, yes, on are
true; anything else is false.Filters apply before paging, so meta.total reflects the filtered count.
Every failure of a resource endpoint returns the same envelope, with the HTTP status repeated inside the body so it survives logging:
{
"error": {
"code": "INVALID_TOKEN",
"message": "Token is invalid, expired, or revoked.",
"status": 401
}
}
Branch on error.code, never on error.message — the wording may be
improved, and we aim to keep the codes stable within a version. The table
under this section lists every code a resource endpoint can return, and what
to do about each.
Two cases are worth calling out because they look like one another:
MISSING_TOKEN means the Authorization header was absent or did not
begin with Bearer . Your request never reached a token lookup.INVALID_TOKEN means a token was sent but did not pass: it does not
exist, it was revoked, it expired, or the store is suspended. The API
deliberately does not tell you which, so a token cannot be probed.Each token may make 60 requests per 60 seconds. The budget is per token, not per IP address, so splitting work across machines does not raise it — creating a second token for a second integration does.
Every successful response carries your current standing:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed per window (60). |
X-RateLimit-Remaining |
Requests left in the current window. |
X-RateLimit-Reset |
Unix timestamp one full window after this request. On a 429 it is when you may retry. |
Over the limit you get 429 with error.code of RATE_LIMITED, plus a
Retry-After header holding the number of seconds to wait. The 429
response repeats X-RateLimit-Limit, X-RateLimit-Remaining (always 0)
and X-RateLimit-Reset.
Honour Retry-After rather than retrying immediately:
response=$(curl -s -D headers.txt "https://api-staging.sappo.xco.agency/theme/v1/scheduled-themes" \
-H "Authorization: Bearer mak_your_token_here")
# on 429: sleep "$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')"
For a large export, prefer a bigger page_size over more requests: one
request for 100 records costs the same budget as one request
for a single record.
The documentation endpoints are not metered by this budget.
The version is the v1 segment in the path, and this documentation
describes release 1.0.0.
Inside v1 we aim to make only additive changes: a new resource, a new
filter, or a new field on a record. Treat unknown fields as something to
ignore rather than something to fail on, and your integration will keep
working.
A change that could break a reader — a field removed or renamed, a filter's
meaning changed, a status value dropped — belongs on a new path segment
(v2), with v1 still answering. Bug fixes are the exception we do make
inside v1: where a list was ordered by the wrong column, correcting it
changes what page one returns.
info.version in the OpenAPI document is the documentation
release; the path segment is the API contract. Pin the path segment in your
code, and re-read the document when info.version changes.
The Model Context Protocol (MCP) is an open standard that lets an AI assistant call tools on a server instead of guessing from text: you connect the assistant once, it discovers which tools exist, and then it can answer "how many claims are still pending?" by actually querying your store rather than inventing a number. Claude Desktop, Claude Code and a growing list of other clients speak it.
Theme Maestro exposes the same data as this API over MCP at https://api-staging.sappo.xco.agency/theme/mcp,
authenticated with the same mak_ token in the same
Authorization header. No separate signup, no second credential.
The tool list, the exact client configuration to paste, and the errors to
expect are on the MCP guide, with a
machine-readable copy at
https://api-staging.sappo.xco.agency/theme/docs/mcp.json.
| Status | Code | Message | What to do |
|---|---|---|---|
| 400 | INVALID_PAGE |
Query parameter page must be a positive integer. | Send |
| 400 | INVALID_PAGE_SIZE |
Query parameter page_size must be an integer between 1 and 100. | Send |
| 401 | MISSING_TOKEN |
Authorization header with Bearer token is required. | Send an |
| 401 | INVALID_TOKEN |
Token is invalid, expired, or revoked. | Check the token was copied in full, is still listed as active in Settings → API Access, and has not passed its expiry date. Create a new token if in doubt. The same code is returned when the store is suspended. |
| 403 | PLAN_REQUIRED |
API access is not included in the current plan. | The token is valid, but this app sells API access on named plans only and this store is not on one of them. The plans that carry it are listed under Authentication above; change the plan in the app and the same token starts working — there is no need to create a new one. |
| 403 | INSUFFICIENT_SCOPE |
This token does not include the {resource}:read scope. | The token is valid and the plan allows the API, but this token was created without the scope for that resource. Scopes are fixed when a token is created, so create a new one that includes it — a token minted before scopes existed carries the wildcard and is unaffected. |
| 404 | APP_NOT_FOUND |
App not found. | The app handle in the path is not one Sappo serves. Use the handle this documentation was generated for, and check for a typo or a stray path segment. |
| 404 | RESOURCE_NOT_FOUND |
Resource '{resource}' is not available for 'theme'. | The app is valid but it does not expose that resource. Use one of the resource names listed in this documentation, or read them from |
| 404 | NOT_FOUND |
No record found for id {id}. | The resource exists but holds no record with that id for your store. Check the id came from this app and this store — ids are not shared between apps. This is the single-record form; a list request with no matches answers 200 with an empty |
| 429 | RATE_LIMITED |
Too many requests. Please slow down. | Wait the number of seconds in the |
| 500 | INTERNAL_ERROR |
An internal error occurred. | The request was accepted but failed on our side; it has been reported to us automatically. Retry once after a short pause, and if it persists contact support with the time of the request and the path you called. |
| 503 | API_NOT_AVAILABLE |
The API is not available for this app yet. | The app is served but its store of API tokens has not been created yet, so no token can be checked. Nothing is wrong with your request: retry later, and tell support if it lasts. |