← API reference

Pagination

Most list endpoints return a standard pagination envelope. The exceptions are dashboard/ugc-impact and gsc/trends, which return the whole result set as data with no limit or offset keys, and checks/answers, which pages by cursor (see "Cursor pagination" below). The standard envelope:

{
  "data": [...],
  "total": 1234,
  "limit": 50,
  "offset": 0
}

Parameters

Parameter Type Default Max Notes
limit integer 50 100 Number of items per page
offset integer 0 - Zero-based starting index

An out-of-range limit (less than 1 or greater than 100) or a negative offset returns 422 VALIDATION_ERROR. checks/answers takes no offset and caps limit at 25.

Example: paging through all prompts

# Page 1
curl -H "Authorization: Bearer zt_live_<token>" \
  "https://api.ziptie.ai/v1/projects/PROJECT_ID/prompts?limit=100&offset=0"

# Page 2
curl -H "Authorization: Bearer zt_live_<token>" \
  "https://api.ziptie.ai/v1/projects/PROJECT_ID/prompts?limit=100&offset=100"

Determining total pages

import math

total = response["total"]
limit = response["limit"]
total_pages = math.ceil(total / limit)

Fetching all items

The prompts, tags, and urls lists apply every filter, the sort and the page in the database, so every row of a project of any size is reachable through offset, and total counts the rows under every filter. The loop below walks them to the end. prompts/performance pages the same way over the prompts its filters keep.

The eight competitor drill-downs - competitors/mentions/{brand}/prompts, competitors/citations/{domain}/prompts, competitors/citations/{domain}/urls, competitors/sources/prompts, competitors/serp/{domain}/prompts, competitors/serp/{domain}/urls, competitors/bing-organic/{domain}/prompts and competitors/bing-organic/{domain}/urls - also report an exact count with no cap: total is every prompt or page behind the ranking row, and the counts summed over every page equal that row's count.

The competitors/citations, competitors/mentions, competitors/sources, competitors/serp and competitors/bing-organic endpoints behave differently: they page over a ranking that is already capped where it is produced, and total counts exactly the rows in that capped ranking. The loop below terminates on them with no empty pages. What total does not tell you is how long the tail below the cap was.

GET /checks and GET /brand-aliases also report exact counts with no cap above them - GET /checks counts (prompt, check) pairs in the window and paginates in the database, so it is safe to walk a whole project's history with the loop below.

GET /competitors also reports an exact row count, and with no cap above it: it pages the project's whole tracked-competitor roster, so total is every mark the project has and the loop below reaches all of them.

dashboard/ugc-impact/{platform} reports an exact count with no cap, of URLs rather than citations: every check that cited a URL, by any engine, merges into one row before the count is taken, so total is smaller than the citations figure the parent ranking shows for that platform and the two are not meant to agree. brand_awareness_score and sentiment_overview on that response are computed over the whole filtered set rather than the page, so they are identical on every page of one walk and there is no reason to re-read them. The loop below terminates with no empty pages.

def fetch_all(base_url, headers, limit=100):
    offset = 0
    while True:
        resp = requests.get(
            f"{base_url}?limit={limit}&offset={offset}",
            headers=headers
        ).json()
        yield from resp["data"]
        offset += limit
        if offset >= resp["total"]:
            break

Paging a changing list

On the offset-paged lists, paging is plain offset arithmetic against live data. Every request re-reads the rows from scratch, re-applies q, re-sorts them, and slices the requested page, so there is no snapshot and no cursor to fall back on. Rows created or deleted while you walk shift every position after them: an insert pushes one row over the page boundary and you fetch it twice, a delete pulls one back and you never see it. total moves under you as well, which changes where the loop above stops.

Three habits make the walk safe:

Cursor pagination: checks/answers

GET /v1/projects/{project_id}/checks/answers is the one /v1/ endpoint that pages by cursor. It returns full engine answers, one item per (prompt, check) pair, in a fixed (prompt_id, check_no) order, so it takes no sort and no offset, and its envelope carries no total:

{
  "data": [...],
  "next_cursor": "4407:18",
  "limit": 25
}

Stop only when next_cursor is null. A page can hold fewer than limit items, or none at all, while more follow, so a short or empty page is not the end of the walk.

In window mode, pass explicit date_from and date_to on every page. Without latest or check_no the endpoint reads a date window, and the default window (30 days before today through today) is recomputed from today's UTC date on each request. A walk that crosses midnight UTC without explicit dates silently gains a day at the new end and loses its oldest. Each page's applied echoes the window it read, so compare it across pages to catch that. With latest=true or check_no, repeat that selection on every page instead.

import requests

url = "https://api.ziptie.ai/v1/projects/PROJECT_ID/checks/answers"
headers = {"Authorization": "Bearer zt_live_<token>"}
# The same explicit dates on every page: the default window moves with the UTC date.
params = {"date_from": "2026-09-01", "date_to": "2026-09-21", "limit": 25}
items, cursor = [], None
while True:
    page = requests.get(
        url,
        params={**params, "cursor": cursor} if cursor else params,
        headers=headers,
    ).json()
    items.extend(page["data"])
    cursor = page["next_cursor"]
    if cursor is None:
        break

A walk reads each pair once and never repeats one. Checks that land while you walk can add pairs: in window mode a new check for a prompt the cursor has already passed is not returned, and one for a later prompt is. A malformed cursor returns 422 VALIDATION_ERROR.

Empty results

When no items match the current filters, data is an empty list and total is 0. This is not an error.

{"data": [], "total": 0, "limit": 50, "offset": 0}

Combined with filters

Pagination parameters combine with the other query parameters an endpoint accepts. Example: fetch the second page of prompts matching a text search, sorted by prompt text:

curl -H "Authorization: Bearer zt_live_<token>" \
  "https://api.ziptie.ai/v1/projects/PROJECT_ID/prompts?q=running&sort=text:asc&limit=50&offset=50"