The v2 API uses three pagination styles, each fitting the underlying data model. Knowing which style applies to which endpoint lets you build a uniform client that handles all three.
| Pattern | Query params | Used by |
|---|
| Offset | take, skip | Most list endpoints (audit logs, reporting, creatives, test cohorts, …) |
| Custom offset | groupOffset, groupLimit, productOffset, productsPerGroup | Discovery (POST /discovery/.../discover-products) |
| Cursor | limit, starting_after | Storefront billing endpoints proxied to Stripe |
The default for v2 list endpoints. take is the page size; skip is the offset from the start.
Request
GET /api/buyer/audit-logs?take=50&skip=100
Authorization: Bearer <SCOPE3_API_KEY>
| Param | Type | Default | Notes |
|---|
take | number | varies | Page size. Most endpoints cap at 100 or 200 |
skip | number | 0 | Records to skip |
Response
The response includes a meta.pagination block alongside data:
{
"data": {
"logs": [/* ...50 items... */],
"total": 1342
},
"error": null,
"meta": {
"pagination": {
"skip": 100,
"take": 50,
"total": 1342,
"hasMore": true
}
}
}
| Field | Type | Meaning |
|---|
skip | number | Echoed offset |
take | number | Echoed page size |
total | number | Total matching rows across all pages |
hasMore | boolean | true if skip + returned < total |
Example: paging through audit logs
async function* listAllAuditLogs() {
const take = 100;
let skip = 0;
while (true) {
const res = await fetch(
`https://api.agentic.scope3.com/api/buyer/audit-logs?take=${take}&skip=${skip}`,
{ headers: { Authorization: `Bearer ${process.env.SCOPE3_API_KEY}` } },
);
const { data, meta } = await res.json();
yield* data.logs;
if (!meta.pagination.hasMore) return;
skip += take;
}
}
Custom offset (discovery)
Discovery returns nested results — product groups, each containing several products — so it exposes two independent offsets. This lets you page through groups without re-fetching products you’ve already seen, and vice versa.
Request
POST /api/buyer/discovery/{discoveryId}/discover-products
Content-Type: application/json
{
"advertiserId": 123,
"groupLimit": 5,
"groupOffset": 0,
"productsPerGroup": 10,
"productOffset": 0
}
| Param | Type | Meaning |
|---|
groupLimit | number | How many product groups to return |
groupOffset | number | Skip this many product groups |
productsPerGroup | number | Products to include within each returned group |
productOffset | number | Skip this many products within each group |
Response
{
"data": {
"discoveryId": "disc_01HX…",
"productGroups": [/* ... */],
"totalGroups": 23,
"hasMoreGroups": true,
"summary": { "totalProducts": 412 }
},
"error": null
}
hasMoreGroups tells you whether to advance groupOffset for another page of groups. summary.totalProducts is the full denormalized total across all groups.
Used by Stripe-Connect billing endpoints, which proxy directly to Stripe and inherit Stripe’s cursor convention.
Endpoints
GET /storefront/billing/transactions
GET /storefront/billing/payouts
Request
GET /api/storefront/billing/transactions?limit=25&starting_after=txn_1Nq…
| Param | Type | Default | Meaning |
|---|
limit | number | 25 | Page size, max 100 |
starting_after | string | — | Object ID returned in a previous page’s meta.cursor |
Response
{
"data": [/* ...transactions... */],
"meta": {
"count": 25,
"limit": 25,
"hasMore": true,
"cursor": "txn_1NqAbc123"
}
}
meta.cursor is the value to pass as starting_after on the next request. When hasMore is false, you’ve reached the end.
async function* listAllTransactions() {
let cursor: string | undefined;
while (true) {
const url = new URL(
"https://api.agentic.scope3.com/api/storefront/billing/transactions",
);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SCOPE3_API_KEY}` },
});
const body = await res.json();
yield* body.data;
if (!body.meta.hasMore) return;
cursor = body.meta.cursor;
}
}
Per-endpoint quick reference
| Endpoint | Style | Page params |
|---|
GET /buyer/audit-logs | Offset | take, skip |
GET /reporting/metrics | Offset | take, skip |
GET /creatives | Offset | take, skip |
GET /test-cohorts | Offset | take, skip |
GET /human-feedback | Offset | take, skip |
GET /measurement-engine/* | Offset | take, skip |
GET /advertiser-accounts | Offset | take, skip |
POST /discovery/:id/discover-products | Custom offset | groupLimit, groupOffset, productsPerGroup, productOffset |
GET /storefront/billing/transactions | Cursor | limit, starting_after |
GET /storefront/billing/payouts | Cursor | limit, starting_after |
When in doubt, inspect the response. Offset endpoints expose
meta.pagination.hasMore; cursor endpoints expose meta.hasMore plus
meta.cursor. Discovery exposes hasMoreGroups directly on data.
Don’t assume total is cheap to compute. For high-cardinality endpoints
(audit logs, reporting metrics) prefer to consume pages with hasMore rather
than computing Math.ceil(total / take) and looping — the count may be an
estimate.