Pagination
Every list endpoint covered by this skill uses opaque cursor pagination — with one exception:GET /v1/canvases/search is offset-paginated (see Search is the offset lane below).
Newer surfaces outside this catalog (the drive folder/file lists, the website list) are offset lanes too, with their own {<name>, limit, offset, has_more} shapes. Read the endpoint’s own reference before assuming the cursor contract.
Response shape
data— the page of items.next_cursor— opaque signed string. Pass back as?cursor=<value>to get the next page.nullmeans you’ve reached the end.returned/has_more— derived server-side (len(data)andnext_cursor != null), so a page can never silently look complete. Never infer the end fromlen(data) < limit.limit— the effective page size the response was computed with.total— see below: a real count on some lanes,nullon others.
Iteration pattern
TypeScript:Limits
Always request as large a page as you can tolerate — fewer roundtrips, less rate pressure.
Sort order
List endpoints sort by(created_at DESC, id DESC) on immutable columns. This guarantees pagination correctness when rows are added or modified mid-scan — no skips, no duplicates — at the cost of “sort by updated_at” being unavailable.
Client-side sort if you need a different order.
total is per-lane
total is present in the envelope but only populated where the collection supports a cheap true count — GET /v1/tasks, GET /v1/brand-kits, and GET /v1/organizations return a real count after filters. It stays null on the lanes where counting is as expensive as the query itself: GET /v1/canvases (the doc-kind filter reads un-indexed JSONB), GET /v1/events, GET /v1/webhook_deliveries, and the relevance-ranked search.
So: use total when you have it, and never require it. has_more is the authoritative “is there more” signal on every lane. For a progress bar on a null-total lane, the common pattern is “processed 127 so far…” without a denominator.
Opaque cursors
Cursors are HMAC-signed + base64url-encoded. Don’t:- Parse or mutate the cursor value.
- Attempt to construct one by hand.
- Reuse a cursor from a different list endpoint.
400 invalid_request.
Cursors are valid across sessions but have a bounded TTL — don’t store one for a week and expect it to resume correctly. For long-running iterations (> a few hours), consider re-starting from the beginning.
Endpoints that paginate
All of these return{data, next_cursor}:
GET /v1/canvasesGET /v1/tasksGET /v1/brand-kitsGET /v1/organizationsGET /v1/eventsGET /v1/webhook_deliveries
GET /v1/canvases/{id}, GET /v1/tasks/{id}, GET /v1/credits, etc.) don’t paginate.
Search is the offset lane
GET /v1/canvases/search is relevance-ranked, so a cursor over (created_at, id) would be meaningless. It pages by offset and uses its own response shape:
canvases, not data. There is no next_cursor — page with offset + limit while has_more is true. limit defaults to 20 and caps at 100, same as the /canvases cursor lane (see Limits — the /events and /webhook_deliveries lanes are 50/200).
Concurrency
Don’t parallelize pagination. Each page depends on the previous one’s cursor. Parallelize the work done per item inside a page instead.Migration from offset pagination
If you’re carrying code from the legacy2026-04-12 shape:
Common wrong guesses
- Assuming
totalis always there — or never there. It is a real count on/tasks,/brand-kits, and/organizations, andnullon/canvases,/events,/webhook_deliveries, and search. Branch on it; rely onhas_more. - Parsing / mutating cursors. Opaque; server rejects tampered values.
- Passing
offset=to a cursor endpoint. Ignored (or rejected, depending on endpoint) on2026-05-01. Use the cursor — except on/canvases/search, whereoffsetis the mechanism. - Holding a cursor for days. Cursors expire; restart the iteration.
- Parallel page fetches. Each page depends on the prior cursor.
- Reading
response.canvases/response.tasks/response.brand_kits. The cursor lanes always put items underdata.GET /canvases/searchis the exception — its items are undercanvases. - Expecting a
next_cursorfrom/canvases/search. It has none. Page withoffsetwhilehas_moreistrue.
Upstream
docs.moda.app/api/versioning— migration map- Per-endpoint docs at
docs.moda.app/api/{canvases,tasks,brand-kits,organizations}/*