API

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. Read payload.data, not payload.tasks or payload.results.
  • links.next is null on the last page and links.prev is null on the first. Follow next until it is null and you have walked the whole collection.
  • pagination.total is the total row count for the query, and pagination.pages the 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.