Incremental sync with updated_since
To keep a copy of Sursa data in your own system, you do not need to read everything every time. The lists of objects
that have updated_at accept updated_since and return only the objects changed at or after that moment.
curl -G https://api.example.com/v1/cards \
-H "Authorization: Bearer $SURSA_API_KEY" \
--data-urlencode "updated_since=2026-10-23T00:00:00Z" \
--data-urlencode "limit=200"With updated_since, the list is sorted by updated_at ascending, then by id, and the cursor
continues in that order. So you read the changes in the order they happened, and a page boundary never splits them
in a way that loses one.
updated_since is accepted by these lists:
| List | What counts as a change |
|---|---|
GET /v1/cards, GET /v1/cards/count | updated_at of the card. |
GET /v1/groups | updated_at of the group: its title or settings. |
GET /v1/webhooks | updated_at of the endpoint: its settings and secret, not its deliveries. |
GET /v1/jobs | updated_at of the job: every change of its status. |
GET /v1/drafts | updated_at of the draft. |
GET /v1/cards/{card_id}/threads | The latest creation or edit of the root comment or of any reply. The thread comes whole. |
GET /v1/folders, GET /v1/collections/{name}/folders | updated_at of the folder. The matching folders come as a flat list, see below. |
Collections, users and announcements have no updated_at, so their lists do not filter by it: they are short, so
read them whole on every run. The time must have a time zone offset, such as Z or +02:00; any other value is
refused, as with GET /v1/cards.
With updated_since, a folder tree is no longer a tree: folders of each collection holds the changed folders as a
flat list, with empty children, and parent_id tells where each one sits. GET /v1/folders
then leaves out the collections without changed folders.
A sync loop
- On the first run, read the whole list without
updated_since, and remember the moment the run started. - On every next run, pass that remembered moment as
updated_since, minus a small margin such as one minute, and remember the new start time. - Write each object into your store by its
id, replacing the previous copy. Objects that appear twice because of the margin are simply written again. - Request every next page with the cursor and the same
updated_since. A cursor works only with the filters of the first page: without them the request returns400with the codeapi.cursor_invalid.
Using the start time of the run, not the time it finished, makes sure that a change made while the run was going is picked up by the next one.
GET /v1/cards returns a JSON array, so its next page comes in the X-Next-Cursor header
(see Lists that return an array):
const BASE = "https://api.example.com";
const headers = { Authorization: `Bearer ${process.env.SURSA_API_KEY}` };
async function syncCards(since, saveCard) {
const startedAt = new Date();
const params = new URLSearchParams({ limit: "200" });
if (since) params.set("updated_since", new Date(since.getTime() - 60_000).toISOString());
let query = params.toString();
while (query) {
const response = await fetch(`${BASE}/v1/cards?${query}`, { headers });
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
for (const card of await response.json()) await saveCard(card); // write into your store by card.id
const cursor = response.headers.get("X-Next-Cursor");
if (cursor) params.set("cursor", cursor); // keep updated_since: the cursor needs the same filters
query = cursor ? params.toString() : "";
}
return startedAt; // pass it as `since` next time
}Deletions
A list only returns objects that exist, so it cannot tell you that something was deleted. Take deletions from the event feed:
curl -G https://api.example.com/v1/webhook-events \
-H "Authorization: Bearer $SURSA_API_KEY" \
--data-urlencode "type=card.deleted" \
--data-urlencode "created_since=2026-10-23T00:00:00Z"Reading the feed needs a key with the admin role and the webhooks:manage scope. Events are kept for 30 days. If
your sync has not run for longer than that, read the full list again and remove from your store every object that is
no longer in it.
Export to a data warehouse puts lists, updated_since and the event feed
together into one job.