https://api-staging.sappo.xco.agency
The Rankify API lets you read your own store's Rankify 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/rankify/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.
Every resource endpoint is a read. Rankify additionally accepts POST https://api-staging.sappo.xco.agency/rankify/v1/connector, which does change your data — see Writing data: the connector below.
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/rankify/v1/collections" \
-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/rankify/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/rankify/v1/collections?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/rankify/docs |
This page |
GET https://api-staging.sappo.xco.agency/rankify/docs.json |
OpenAPI 3.0 document |
GET https://api-staging.sappo.xco.agency/rankify/docs/content.json |
This documentation as JSON |
GET https://api-staging.sappo.xco.agency/rankify/docs/mcp |
The MCP guide |
GET https://api-staging.sappo.xco.agency/rankify/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/rankify/v1/collections?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/rankify/v1/collections?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.
Rankify is the one part of this API that changes your data, and it does so through a single endpoint rather than through the resources above:
POST https://api-staging.sappo.xco.agency/rankify/v1/connector
It takes the same Authorization: Bearer mak_… header, is scoped
to the same store, and spends the same rate-limit budget. The body names an
action and its parameters:
curl -X POST "https://api-staging.sappo.xco.agency/rankify/v1/connector" \
-H "Authorization: Bearer mak_your_token_here" \
-H "Content-Type: application/json" \
-d '{"action": "enable_collection", "params": {"collection_id": "123"}}'
Scopes narrow this. The token screen lets you choose whether a token may
make changes, and this endpoint checks that choice: an action below only
runs for a token that carries the connector:write scope. A token without it
is refused with 403 INSUFFICIENT_SCOPE and the message This token does
not include the connector:write scope — including for the actions that
only read, because the whole endpoint sits behind the one scope.
Tokens minted before scopes were enforced carry * and still pass. Keep
one token per integration, give it no more than it needs, and revoke one
you no longer use.
| Action | Required parameters | Changes data |
|---|---|---|
list_collections |
— | no |
get_collection |
collection_id |
no |
enable_collection |
collection_id |
yes |
disable_collection |
collection_id |
yes |
sort_collection |
collection_id |
yes — queues a re-sort |
set_product_boost |
product_id, boost |
yes |
list_recipes |
— | no |
bulk_toggle_collections |
collection_ids, status |
yes |
bulk_assign_recipe |
collection_ids, metrics_id |
yes |
list_products |
— | no |
get_serve_usage |
— | no |
The connector has its own envelope, not the data/meta one the
resources use. On success:
{ "success": true, "data": { "collection_id": "123", "enabled": true } }
On failure — note that error is true here rather than an object, and
that the code is lower case:
{ "error": true, "message": "Collection not found", "code": "not_found" }
| Status | code |
Meaning |
|---|---|---|
404 |
not_found |
The collection or recipe is not one of yours. |
422 |
invalid_action, invalid_params |
The action name is unknown, or a required parameter is missing or empty. |
Three more codes are mapped but not returned today: unauthorized would
answer 401, permission_denied 403, and anything else 400. The store
is resolved from your token before an action runs, and a token that fails
is answered by the same middleware as a GET, so handling these is forward
planning rather than something you will see. A product boost is the one
action that does not check its id: any product_id is accepted.
A missing or invalid token, an unknown app handle and the rate limit are
handled by the same middleware as a GET, so those come back in the
error object envelope described under Errors.
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.
The connector endpoint is the exception: its own failures come back in the envelope described under Writing data: the connector, with lower-case codes that appear nowhere in this table.
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/rankify/v1/collections" \
-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.
Rankify exposes the same data as this API over MCP at https://api-staging.sappo.xco.agency/rankify/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/rankify/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 'rankify'. | 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. |