Preskočiť na obsah

Pagination and sync

List endpoints return one page at a time. This page explains the page envelope, the filters that make polling and incremental sync cheap, and the rules that keep a sync loop correct.

A collection returns {"data": [...], "next_cursor": "...", "has_more": true}. On the last page next_cursor is null and has_more is false. An empty result is an empty array, never an error. Read the next page by passing next_cursor as the cursor query parameter. The list of products is the one exception: it keeps its older shape with next_cursor and an optional total, and it has no has_more.

limit accepts 1 to 100 and defaults to 50; anything else returns 400 INVALID_PARAMETER. sort takes one of the fields listed for the endpoint, with a leading - for descending order, and defaults to -created_at. The cursor is opaque and is bound to the sort and filters of the request that produced it. Using it with different filters or a different sort returns 400 INVALID_CURSOR, so keep all parameters identical while paging and change only cursor.

created_after returns items created at or after the given instant, in ISO 8601 (for example 2026-10-11T08:00:00Z; a plain date or a date-time without zone is read as UTC). Because the boundary is inclusive, the newest item you already have comes back once more, so always deduplicate by id. Sort ascending with sort=created_at so that a new item can never shift the pages you have not read yet.

updated_since returns items changed at or after the instant, which is what an incremental sync needs. Sort by updated_at ascending, remember the largest updated_at you processed, and use it as the next updated_since. A resource without an updated_at rejects the parameter with 400 UNSUPPORTED_FILTER. The filter lists items that still exist, so detect removals with webhooks such as lead.archived, or reconcile with a full read from time to time.

Every resource has a stable id (UUID; products use uuid). Treat the read as an upsert keyed by id: the same item can legitimately appear twice, once on the boundary of two syncs and once after an edit. Webhook deliveries are at least once as well, and their envelope carries a stable event id for the same purpose (see Webhooks).

This reads every lead changed since a checkpoint and returns the new checkpoint. It needs Node.js 18 or newer and the scope leads:read.

const BASE = "https://api.dakoty.com/api/v1/public";
const headers = { Authorization: `Bearer ${process.env.DAKOTY_API_KEY}` };
async function* listAll(path, params = {}) {
let cursor = null;
do {
const qs = new URLSearchParams({ limit: "100", ...params });
if (cursor) qs.set("cursor", cursor);
const res = await fetch(`${BASE}${path}?${qs}`, { headers });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const page = await res.json();
yield* page.data;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}
export async function syncLeads(checkpoint, store) {
let latest = checkpoint;
for await (const lead of listAll("/leads", { updated_since: checkpoint, sort: "updated_at" })) {
store.set(lead.id, lead);
if (lead.updated_at && lead.updated_at > latest) latest = lead.updated_at;
}
return latest;
}

The same loop with requests. Keep params unchanged between pages so the cursor stays valid.

import os
import requests
BASE = "https://api.dakoty.com/api/v1/public"
HEADERS = {"Authorization": f"Bearer {os.environ['DAKOTY_API_KEY']}"}
def list_all(path, **params):
cursor = None
while True:
query = {"limit": 100, **params}
if cursor:
query["cursor"] = cursor
r = requests.get(BASE + path, headers=HEADERS, params=query, timeout=30)
r.raise_for_status()
page = r.json()
yield from page["data"]
if not page["has_more"]:
return
cursor = page["next_cursor"]
def sync_leads(checkpoint, store):
latest = checkpoint
for lead in list_all("/leads", updated_since=checkpoint, sort="updated_at"):
store[lead["id"]] = lead
latest = max(latest, lead["updated_at"] or latest)
return latest

To check whether a record already exists before creating it, use the /find endpoints for companies, contacts, leads and invoices. They do an exact match, need at least one query parameter (for example registration_id, email or phone for companies) and return an empty list when nothing matches. Creating a lead can also deduplicate on its own with dedupe=company; see the API reference.