Envelope & pagination
Every GET endpoint on api.saaslivery.com returns one of two envelopes: a single resource or a paginated collection. The shapes are identical across every app, core and third-party alike, so a client that can read one endpoint can read them all.
Single resources
{
"data": {
"id": "task_8f2k1",
"title": "Ship the Q3 report",
"status": "open"
},
"links": {
"self": "/tasks/task_8f2k1"
}
}
The resource is always under data. The links object carries at least self, plus any relationship links the endpoint exposes.
Collections
{
"data": [
{"id": "task_8f2k1", "title": "Ship the Q3 report"},
{"id": "task_9x4m2", "title": "Review the pricing page"}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 68,
"pages": 3
},
"links": {
"self": "/tasks?page=1&per_page=25",
"first": "/tasks?page=1&per_page=25",
"last": "/tasks?page=3&per_page=25",
"next": "/tasks?page=2&per_page=25",
"prev": null
}
}
Rules a client can rely on:
- The items are always under
data, never under a resource-specific key. Readpayload.data, notpayload.tasksorpayload.results. links.nextisnullon the last page andlinks.previsnullon the first. Follownextuntil it isnulland you have walked the whole collection.pagination.totalis the total row count for the query, andpagination.pagesthe page count at the current page size.
Pagination parameters
| Parameter | Default | Maximum | Notes |
|---|---|---|---|
page |
1 |
none | 1-indexed; values below 1 are clamped to 1 |
per_page |
25 |
100 |
values above the maximum are clamped to 100 |
curl "https://api.saaslivery.com/tasks?page=2&per_page=50" \
-H "Authorization: Bearer slk_..." \
-H "X-Api-Version: 2026-03-15"
Malformed values fall back to the defaults rather than erroring, so a bad page=abc gives you page 1, not a 400.
Building endpoints that follow the envelope
If you are writing an app, never hand-build these shapes. Use the platform helpers in core.responses:
from http import HTTPStatus
from core.responses import many, one, parse_pagination
async def api_list_items(req: Request, res: Response, ctx: Context):
page, per_page = parse_pagination(req)
items, total = await fetch_items(ctx, page, per_page)
return res.out(HTTPStatus.OK, many(
items, total=total, page=page, per_page=per_page, path="/items",
))
async def api_get_item(req: Request, res: Response, ctx: Context):
item = await fetch_item(ctx, req.params["id"])
return res.out(HTTPStatus.OK, one(item, links={"self": f"/items/{item['id']}"}))
many() computes the pagination block and the first/last/next/prev links from total, page, and per_page. Pass extra_links when a collection should advertise related resources.