Change feed
Poll GET /v1/changes with a cursor to read every Workwae event in order, as an alternative to webhooks.
The change feed, GET /v1/changes, lists your tenant's events oldest first. Poll it with a cursor you store, and you read every event exactly in order, without exposing a public endpoint.
It carries the same events as webhooks, in the same shape and with the same ids. Test events never appear in it.
Webhooks or the change feed
| Choose | When |
|---|---|
| Webhooks | You want events within seconds, pushed and retried, and can expose a public HTTPS endpoint. |
| Change feed | You cannot receive inbound requests, want to process in strict order at your own pace, or need to catch up after downtime from a stored position. |
You can use both: an event's id is the same in each, so deduplicate on it.
Read the feed
curl "https://api.workwae.com/v1/changes?limit=100" \
-H "Authorization: Bearer $WORKWAE_API_KEY"{
"items": [
{
"id": "1001",
"type": "message.created",
"accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
"createdAt": "2026-05-04T09:30:00.000Z",
"data": { "message": { "id": "c4e6a8b0-…" }, "mailboxes": [] }
}
],
"nextCursor": "MTAwMQ"
}| Parameter | Description |
|---|---|
cursor | The previous page's nextCursor. Omit it to start from the oldest retained event. |
limit | Events per page, 1 to 200 (default 50). |
types | Only these event types: comma separated, or the parameter repeated. |
Cursors and empty pages
nextCursoris never null: the feed has no end. Always store it and pass it back ascursor.- An empty page means you are caught up. Keep the same cursor and poll again later.
- With
typesset,nextCursormay move past events that did not match, so the next request does not rescan them. - A cursor Workwae did not produce answers with reason
invalid_cursor.
Retention
Events are kept for the tenant's eventRetentionDays setting, 7 to 30 days. The feed starts at the oldest event still retained.
Poll within the retention window
A cursor older than the retention window continues from the oldest retained event: events in between are gone. Poll at least once per retention period.
Account-limited API keys
An API key limited to some accounts reads only those accounts' events. Tenant-level events (accountId null) are left out.
Polling loop
Save the cursor after handling each page, so a restart resumes where it stopped. Wait before polling again when a page is empty.
import { setTimeout as sleep } from "node:timers/promises";
let cursor = await loadCursor(); // undefined the first time
for (;;) {
const page = await workwae.changes.list({ cursor, limit: 100 });
for (const event of page.items) {
await handle(event); // { id, type, accountId, data, createdAt }
}
cursor = page.nextCursor;
await saveCursor(cursor);
if (page.items.length === 0) await sleep(5_000);
}The SDK's changes helper iterates events until the feed is caught up and exposes the position to save:
import { changes } from "@workwae/sdk";
const feed = changes(workwae.changes.list, { cursor: await loadCursor() });
for await (const event of feed) await handle(event);
await saveCursor(feed.cursor);See the changes reference for the full operation.