Pagination and filtering
Every list endpoint pages the same way: ask for up to 100 items, get back a cursor, send the cursor to get the next batch. There is no page number and no total count, and both omissions are deliberate — this page explains what you get in exchange and how to traverse a list safely.
The two parameters
| Parameter | Default | Description |
|---|---|---|
limit | 25 | Items per page. Accepts 1–100. Values outside that range are rejected, not clamped. |
cursor | none | An opaque token from the previous response. Omit it to start at the beginning. |
First page
GET /timers?limit=25&sort=-created_at
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Product launch",
"type": "fixed",
"status": "published"
}
],
"page": {
"has_more": true,
"next_cursor": "eyJ2YWx1ZSI6Ii4uLiJ9"
},
"request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}Next page
GET /timers?limit=25&sort=-created_at&cursor=eyJ2YWx1ZSI6Ii4uLiJ9
# Carry every filter and the sort forward unchanged.
# Changing them mid-traversal invalidates the cursor.Stop when page.next_cursor is null. The companion field page.has_more says the same thing as a boolean; use whichever reads better in your loop, but do not infer the end from a short page — a full page can still be the last one.
Why there is no page number
Offset paging — ?page=3 or ?offset=50 — is easier to explain and quietly wrong on any list that changes while you read it.
Sort timers newest-first, read page one, and then create a timer before you read page two. Everything shifts down by one position, so the item that was last on page one is now first on page two: you read it twice. Delete one instead and an item moves up past the boundary, so you never see it at all. Neither failure raises an error, and both corrupt a sync job in ways that surface days later.
A cursor encodes a position in the sort order rather than a count of skipped rows, so inserts and deletes elsewhere in the list do not move your place. In exchange you give up jumping to an arbitrary page and you give up a total count — computing one means scanning the whole table on every request, which gets slower exactly as an account gets larger.
Filtering and sorting timers
GET /timers accepts these alongside the paging parameters. Filters combine with AND.
| Parameter | Accepted values | Notes |
|---|---|---|
type | fixed · duration · recurring · personalized | Restrict to one timer type. |
status | draft · published · archived | Archived timers are excluded unless you ask for them. |
sort | created_at · -created_at · updated_at · -updated_at | A leading minus means descending. Defaults to -created_at. |
?status=published&type=fixed may turn a twenty-page traversal into a single request, and it counts as one call against your rate limit instead of twenty.Keep parameters stable across a traversal
The cursor was issued for one specific query. Changing sort or any filter while paging produces a cursor mismatch, and the sensible response to that is to start the traversal again rather than to guess.
Reading an entire list
A generator is the right shape here: the caller sees a flat sequence, memory stays flat regardless of account size, and the paging stays in one place.
Node.js — async generator
async function* allTimers(filters = {}) {
let cursor = undefined;
do {
const query = new URLSearchParams({ limit: "100", ...filters });
if (cursor) query.set("cursor", cursor);
const { data, page } = await call(`/timers?${query}`);
// Yield as you go. Accumulating 10,000 timers into one array
// just to return it is how a sync job runs out of memory.
for (const timer of data) yield timer;
cursor = page.next_cursor ?? undefined;
} while (cursor);
}
// Usage
for await (const timer of allTimers({ status: "published" })) {
await reconcile(timer);
}Python — generator
def all_timers(session, **filters):
cursor = None
while True:
params = {"limit": 100, **filters}
if cursor:
params["cursor"] = cursor
payload = session.get(f"{BASE}/timers", params=params).json()
yield from payload["data"]
cursor = payload["page"]["next_cursor"]
if not cursor:
returnUse limit=100 for bulk traversal. It is four times fewer requests than the default and the response is not meaningfully slower, which matters when the per-minute rate limit is the constraint.
Retry-After — see rate limits.The other paginated endpoints
The same limit and cursor mechanics apply everywhere. Only the filters differ.
| Endpoint | Returns | Useful filters |
|---|---|---|
GET /timers | Timers on the account | type, status, sort |
GET /timers/{id}/activity | Changes and control actions for one timer | — |
GET /webhook-deliveries | Delivery attempts across destinations | status |
GET /webhook-destinations | Registered destinations | — |
GET /timers/{id}/webhook-rules | Rules attached to one timer | — |
GET /usage/requests | Your API request log | status, route, key, request ID |
GET /timezones | Accepted IANA identifiers | search |
GET /webhook-deliveries?status=failed is the one to reach for first when a webhook integration misbehaves. It shows what we attempted, what your endpoint answered, and when the next retry is due — see retries and replay.