Pagination
How to read long lists: limit, cursor, has_more and stable ordering.
One model
Every list is paged the same way: you send limit and cursor and you get next_cursor back.
{
"object": "list",
"data": [],
"has_more": true,
"next_cursor": "eyJ2IjoxLCJrIjoiMjAyNi0xMC0xMSJ9.k3J9mQ"
}| Field | Meaning |
|---|---|
data |
the items of this page |
has_more |
whether more items follow |
next_cursor |
cursor for the next page. In lists it is null when has_more is false; in the event stream it is always returned |
limit
| List | Default | Range |
|---|---|---|
| most lists | 20 | 1 to 100 |
transcript segments (format=json) |
200 | 1 to 1000 |
| events | 20 | 1 to 100 |
An invalid value returns 400 invalid_parameter with error.param: "limit".
cursor
- A cursor is opaque: do not try to parse or build one. Send exactly what you got from
next_cursor. - List cursors are tied to the user, the route, the sort order and the filters. If you change the filters or the sort order between pages, the API returns 400
invalid_cursor. Then start again without acursor. - List cursors are meant for one paging run. Do not store them for long.
- The event stream cursor is a position in the stream, so you can store it and continue later.
cursor=lateststarts from now: an empty page with the currentnext_cursoris returned.
Ordering
Ordering is stable: by the chosen key and then id, so items neither repeat nor go missing between pages.
| List | Sort key |
|---|---|
GET /meetings |
created_at (default) or updated_at (sort); direction order=desc (default) or asc |
GET /library/transcripts |
added_at; direction order |
GET /library/imports |
created_at, newest first; lists the imports of the last 90 days |
GET /events |
the order in which events were committed |
Filter rules:
- Repeated filters are repeated:
status=ready&status=failed. - Dates are an RFC 3339 date or date-time. A date-time needs a time zone (
2026-10-11T09:30:00Zor+03:00); a bare date such as2026-10-11means 00:00 UTC. Bounds are exclusive:_aftermeans “later than”,_beforemeans “earlier than”. - Bounds are compared in milliseconds, the resolution of the timestamps in responses. So you can pass back a timestamp you received (
updated_after=<updated_at of the last item>) and you never get that same item again; an item that shares the millisecond of your bound is not returned either. Rows changed by one operation (archiving several meetings at once, for example) often share a single timestamp, so never rely onupdated_afteralone to move from one page to the next. updated_afterandupdated_atonly filter; the order of the list still followssortandorder.- Incremental sync. Inside one run, read the pages in order (
sort=updated_at&order=asc) by followingnext_cursoruntilhas_moreisfalse: the order is the key and thenid, so items that share a timestamp are neither repeated nor skipped. Useupdated_afteronly to start the next run: send the largestupdated_atyou saw, moved back by a safety margin (a minute is plenty), and skip the items you have already processed byid. A page that comes back empty is not a reliable end of the run. If you must not miss any change, readGET /eventsinstead.
Reading every item
import os
import requests
BASE = "https://api.diko.app/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DIKO_API_KEY']}"}
def paginate(path, params=None):
params = {"limit": 100, **(params or {})}
while True:
response = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=30)
response.raise_for_status()
page = response.json()
yield from page["data"]
if not page["has_more"]:
return
params["cursor"] = page["next_cursor"]
for meeting in paginate("/meetings", {"status": ["ready"]}):
print(meeting["id"], meeting["title"])const BASE = "https://api.diko.app/api/v1"
const headers = { Authorization: `Bearer ${process.env.DIKO_API_KEY}` }
async function* paginate<T>(path: string, params: Record<string, string> = {}): AsyncGenerator<T> {
let cursor: string | null = null
do {
const query = new URLSearchParams({ limit: "100", ...params, ...(cursor ? { cursor } : {}) })
const response = await fetch(`${BASE}${path}?${query}`, { headers })
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`)
const page: { data: T[]; has_more: boolean; next_cursor: string | null } = await response.json()
yield* page.data
cursor = page.has_more ? page.next_cursor : null
} while (cursor)
}
for await (const meeting of paginate<{ id: string; title: string }>("/meetings", { status: "ready" })) {
console.log(meeting.id, meeting.title)
}Transcripts and revision
Pages of transcript segments carry a revision field in the list envelope. The cursor is tied to it: if the transcript changes while you page (for example because grammar corrections were approved), the API returns 409 transcript_changed. Then read the transcript again without a cursor. If you need the whole text in one call, use format=text, srt or vtt.
The event stream
GET /events uses the same model but always returns next_cursor: even when has_more is false, store it and start from it next time. Only committed events are shown, in commit order, so none is skipped.
- Events are kept for 30 days. Without a
cursorthe stream starts at the oldest event still kept. - Diko records events only while the account has a key that is not revoked or an enabled webhook endpoint. Meetings and imports that finished earlier produce no events, so the stream does not back-fill history. Read history with the list endpoints.
- When the page has events,
next_cursorpoints at the last of them. - When the page is empty,
next_cursoris the current position of the stream (never behind the cursor you sent), so a poller that stores it does not scan the same range again. - A cursor that is not an event cursor (not
latestand not one this API gave you) returns 400invalid_cursor.
More: Events and webhooks.