Working with the API
Pagination
How each list on the platform host is paged — the four ways in use, as they are — and which lists are not paged at all.
A list answers a page at a time and says where the next page starts. The platform's lists do not all say it the same way: four ways are in use, each kept as it was first published, because an integration that reads one would break if it changed under it (Versioning). This page says which list speaks which.
Every way has the same two rules. Send the value back unchanged — it is the list's own bookmark, not a value to build. Keep the same filters from one page to the next; a cursor is a place in one filtered order.
#The four ways
| Send | Read back | Lists | The last page |
|---|---|---|---|
cursor | next_cursor | GET /v1/models, every Community and Cinema Studio list, GET /v1/jobs and GET /v1/chat/conversations | next_cursor is null — or, on GET /v1/jobs, absent |
before | next_before | GET /v1/jobs and GET /v1/chat/conversations, the older way | next_before is absent on GET /v1/jobs, null beside has_more: false on threads |
after | next_after | GET /v1/chat/conversations/{id}/messages | next_after is null beside has_more: false |
| nothing | — | GET /v1/changelog, GET /v1/webhooks, GET /v1/community/me/collections | Every row is in the one answer |
Each paged list takes limit, the page's size, up to a ceiling of its own; its page in the
API reference says both.
#cursor and next_cursor
The value is opaque: a place in the list's order, not an id to read anything by. On the Community's and
Cinema Studio's lists it is the database's own, and one that is not a place in that list is refused
400 invalid_cursor.
GET /v1/models is not paged unless you ask: with no limit it answers every model, and
next_cursor is null. Ask for limit and page by cursor; expand=openapi asks for a page by
itself. The cursor there is a sort position, so a model withdrawn between two pages does not break the
walk, and one added before the cursor between two pages is not seen.
On GET /v1/jobs and GET /v1/chat/conversations the cursor names the last row a page read, to the
microsecond, and the row's own id. The next page starts right after that row, so two jobs or two
threads made in the same microsecond on either side of a page's edge are both listed. A cursor that is
not one a page answered — or one sent beside before — is 400 invalid_cursor.
#before and next_before
The older way on the same two lists, still answered exactly as it was: next_before is the
created_at of the last row the page read, and the next page starts strictly older than it. A job or a
thread made in the same microsecond as that row, and left past the page's edge, is not listed on
either page — which is why cursor was added beside it. Prefer cursor. GET /v1/jobs prints
next_before only when there is another page and has no has_more; threads print has_more and a
next_before that is null on the last page.
A typed client that reads next_before as a date can also lose its microseconds on the way back, and
with them any row of the same millisecond. next_cursor is a string, and nothing parses it.
#after and next_after
A thread's messages are in the order they were written, oldest first, and next_after is the seq of
the last message on the page — a whole number. Send it back as after. A rewound message is not the
thread's any more and is never listed.
#Lists with no pages
GET /v1/changelog answers the newest entries we published, up to a fixed number, and takes no
cursor. GET /v1/webhooks answers every endpoint the account registered, and
GET /v1/community/me/collections every collection the account keeps, private ones included — its
next_cursor is always null.