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

ChooseWhen
WebhooksYou want events within seconds, pushed and retried, and can expose a public HTTPS endpoint.
Change feedYou 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"
Response
{
  "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"
}
ParameterDescription
cursorThe previous page's nextCursor. Omit it to start from the oldest retained event.
limitEvents per page, 1 to 200 (default 50).
typesOnly these event types: comma separated, or the parameter repeated.

Cursors and empty pages

  • nextCursor is never null: the feed has no end. Always store it and pass it back as cursor.
  • An empty page means you are caught up. Keep the same cursor and poll again later.
  • With types set, nextCursor may 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.

On this page