Docs / Tools

Pagination and filtering

How to page through jobs, schedules and runs with cursors, and how to narrow list results with filters and sort orders.

Last updated May 12, 2026

#How pagination works

Every list endpoint in the Tend API (/jobs, /schedules, /runs and /webhooks/deliveries) uses cursor-based pagination. You pass a limit and, for every page after the first, the cursor value returned in the previous response's next_cursor field. When next_cursor is null, you have reached the end of the collection.

The limit parameter defaults to 50 and accepts a maximum of 200. Values above 200 are rejected with a 400 invalid_request error rather than silently clamped, because silent clamping has a long history of producing code that works until the day it doesn't.

JSON
{
  "data": [
    { "id": "job_01J8Q4M2ZT7K9XW3R5B6N0CDEF", "status": "succeeded", "created_at": "2026-05-11T14:02:19Z" },
    { "id": "job_01J8Q4M1V8H2P6YD4A9S3G7KTB", "status": "failed", "created_at": "2026-05-11T14:01:53Z" }
  ],
  "has_more": true,
  "next_cursor": "cur_eyJ0IjoiMjAyNi0wNS0xMVQxNDowMTo1M1oiLCJpIjoiam9iXzAxSjhRNE0xIn0"
}

#Fetching a page

The first request omits cursor. Every subsequent request passes the next_cursor from the previous response, unchanged, along with the same filters and limit. Changing filters mid-iteration returns a 400 invalid_request error because the cursor is bound to the query that produced it.

cURL
curl "https://api.tendcomputer.com/v2/jobs?limit=100" \
  -H "Authorization: Bearer tnd_live_9fK2xQ7mVb3LpR8dWc1Zh"

# Next page
curl "https://api.tendcomputer.com/v2/jobs?limit=100&cursor=cur_eyJ0IjoiMjAyNi0wNS0xMVQxNDowMTo1M1oiLCJpIjoiam9iXzAxSjhRNE0xIn0" \
  -H "Authorization: Bearer tnd_live_9fK2xQ7mVb3LpR8dWc1Zh"

#Iterating over an entire collection

If you want every record, you do not need to write the loop yourself. Each SDK exposes an auto-paginating iterator that fetches pages lazily and stops when next_cursor is null. Pages are requested one at a time, so a slow consumer does not cause the SDK to buffer the whole collection in memory.

Python
for run in client.runs.list(status="failed", limit=200).auto_paging_iter():
    print(run.id, run.job_id, run.error_code)

#Filtering results

Filters are query parameters. Multiple filters combine with AND. Repeating a parameter, as in status=failed&status=retrying, combines the values with OR within that field. Unknown parameters are rejected instead of ignored, which catches typos early.

ParameterApplies toDescription
statusjobs, runsOne of scheduled, queued, running, retrying, succeeded, failed, cancelled. Repeatable.
schedule_idjobs, runsOnly records created by the given schedule, for example sch_01J7Z3K8PQ2W4M6V9XD1CTRHNB.
job_idrunsOnly runs belonging to one job.
created_after, created_beforejobs, runsRFC 3339 timestamps. The range is half-open: after is exclusive, before is inclusive.
regionjobs, runsOne of us-east, us-west, eu-central, ap-southeast.
tagjobs, schedulesExact match on a job or schedule tag. Repeatable.
nameschedulesExact match. Schedule names are unique per environment, so this returns zero or one result.
cURL
curl -G "https://api.tendcomputer.com/v2/runs" \
  -H "Authorization: Bearer tnd_live_9fK2xQ7mVb3LpR8dWc1Zh" \
  --data-urlencode "status=failed" \
  --data-urlencode "status=retrying" \
  --data-urlencode "region=eu-central" \
  --data-urlencode "created_after=2026-05-01T00:00:00Z" \
  --data-urlencode "limit=200"

#Sorting and result stability

All list endpoints return newest records first by default. Pass order=asc to reverse this. The sort key is always created_at with the record ID as a tiebreaker, so two jobs created in the same millisecond still have a deterministic order. Other sort keys are not supported; a filtered query on an indexed field is almost always what you actually wanted.

Cursors give you stable iteration in the face of concurrent writes. A record created while you are paging newest-first will not shift later pages and cause duplicates, because your position is anchored to a specific (created_at, id) pair rather than an offset. The trade-off is that records created after your first request may be missed when sorting descending. If you need a complete snapshot, pin the upper bound with created_before set to the time of your first request.

  • Use order=asc with created_after to build incremental syncs: remember the newest created_at you processed and start from it next time.
  • Runs are immutable once they reach a terminal status (succeeded, failed, cancelled), so incremental syncs of finished runs will not miss updates.
  • Jobs are mutable. A job you already processed can change status, so re-fetch by ID when you need current state.

#Errors you may hit while paging

HTTPCodeTypical cause while paging
400invalid_requestlimit above 200, an unknown filter name, a malformed timestamp, or a cursor reused with different filters. The details field lists the offending parameters.
401invalid_api_keyThe key was revoked mid-iteration. Rotate and restart from the beginning.
404resource_not_foundA schedule_id or job_id filter references a resource in the other environment. Keys from tnd_dev_ and tnd_live_ cannot see each other's resources.
429rate_limit_exceededThe project exceeded its requests-per-minute limit. Sleep for Retry-After seconds, then resume with the same cursor.

Cursors remain valid for 24 hours after issue. A rate-limited walk can safely pause and resume with the last cursor you received. An expired cursor returns 400 invalid_request with cursor in details; restart the walk with created_before set to the oldest created_at you saw.