Skip to main content

Pagination

Every list endpoint returns the same envelope:

{
"data": [ ... ],
"has_more": true,
"next_cursor": "MjAyNi0wMy0wMVQwMDowMDowMC4wMDBafGFiYw"
}
ParameterDefaultNotes
limit25Maximum 100. A larger value is clamped, not refused.
starting_afterThe next_cursor from the previous response.

Keep requesting while has_more is true, passing the previous next_cursor as starting_after. When has_more is false, next_cursor is null and you have everything.

The cursor is opaque

It is a base64 string encoding a position. Do not parse it, construct one, or store it as if it identified a record — its contents will change, and a client that depends on their shape will break when they do.

Why not page numbers

Offset pagination is wrong for a syncing client in two ways, and both only show up in production.

It skips records. Ask for page 1, then page 2. If someone creates a record in between, every later row shifts forward by one — and the row that was last on page 1 is now first on page 2, so the row that would have been first on page 2 is never returned. Nobody notices until a reconciliation finds a person missing.

It gets slower the deeper you go. OFFSET 10000 still makes the database walk ten thousand rows it is going to throw away.

A cursor names a position instead of counting from the start, so neither happens. The trade is that you cannot jump to "page 7" — which no integration actually needs, and every human UI should be doing differently anyway.

Incremental sync

Refetching everything nightly works until it doesn't. Most list endpoints accept updated_since, an RFC 3339 timestamp, and return only records that changed at or after it:

curl "https://app.operentra.com/api/v1/employees?updated_since=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer $OPERENTRA_API_KEY"

Store the time you started the sync, not the time it finished, and use it as the next run's updated_since. Overlapping slightly is free — you already have those records and will simply write them again — while a gap loses changes permanently.

Two limits to design around:

It tracks the record, not its children. An employee's updated_at moves when the employee changes. It does not move when a related record elsewhere changes. Where a child is worth syncing, it is its own resource with its own updated_since.

Some resources have no updated_at at all, because the rows are written once and never edited — a punch, a shift assignment. They publish no updated_at and accept no updated_since; page them by cursor and filter on the field that suits (punched_after, for instance). Inventing an updated_at from the creation time would let you believe you had a change feed that does not exist.

Deletions

A record that is deleted stops appearing in list results, which means a client mirroring the data cannot tell a deletion from a filter change.

Where a resource supports soft deletion, pass include_deleted=true and deleted records come back carrying deleted: true. A sync that must observe removals has to use it — otherwise a departed employee stays in your mirror for ever.

curl "https://app.operentra.com/api/v1/employees?updated_since=2026-09-01T00:00:00Z&include_deleted=true" \
-H "Authorization: Bearer $OPERENTRA_API_KEY"

Ordering

Lists are ordered newest-first by creation. That ordering is what the cursor walks, and it is stable: a record created while you are paging appears at the start, which you have already passed, rather than shifting the rows ahead of you.