init.Tasks openapi.json inittasks.com

cursors, page size, and reading only what changed

Pagination and filtering

Every list route is cursor-paged and returns the same envelope.

json
{
  "data": [],
  "next_cursor": "eyJzIjoiOEU0RjJBMUIifQ"
}

A cursor is an opaque string. Send it back as ?cursor= to get the next page. On a paged list next_cursor is always present, and null means you have the whole list.

A few short lists are not paged and omit the key entirely: GET /v1/webhooks answers with every subscription the account has (25 at most), and a note's own lists, GET /v1/notes/{id}/links, /places, /note-links and /versions, answer in full. GET /v1/sessions answers every session on one page, with next_cursor always null. Everything else pages, including GET /v1/search and GET /v1/webhooks/{id}/deliveries.

A cursor names a position, not a row: the next page is the rows that sort strictly after it. So a cursor is never not_found, and a walk whose cursor row is deleted between pages keeps going. An empty page does mean you reached the end. What a walk still cannot promise is covered in keeping a copy in sync.

Walking a list

bash
cursor=""
while :; do
  page=$(curl -s "https://api.inittasks.com/v1/todos?limit=200${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $INITTASKS_TOKEN")
  printf '%s\n' "$page" | jq -c '.data[]'
  cursor=$(printf '%s' "$page" | jq -r '.next_cursor // empty')
  [ -n "$cursor" ] || break
done

limit defaults to 50 and caps at 200.

Never build a cursor, parse one, or store one as a bookmark between runs. It is only meaningful against the list that issued it, and a string this API did not issue is validation_failed with the pointer at /cursor.

Reading only what changed

?updated_since= takes an RFC 3339 instant and filters on the server-side update time.

bash
curl -s "https://api.inittasks.com/v1/todos?updated_since=2026-09-15T17:00:00Z" \
  -H "Authorization: Bearer $INITTASKS_TOKEN"

Keep the timestamp of your last successful sweep and pass it next time. A full re-read on a timer is the usual reason an integration hits a rate limit.

The update time is not encrypted, which is why the server can filter on it. Nothing about your content can be filtered server-side. See encryption.

Filtering on content

/v1/search and /v1/filters/{id}/todos decrypt inside your request and match there. They work, and they are not a database index. Expect them to cost more than a list, and expect account_too_large on very large accounts.

Ordering

Each list has one order, and it is the same value the cursor resumes in.

list order
/todos, /notes, /containers, /inbox, /tags, /filters, /attachments, /filters/{id}/todos (sortKey, id)
/trash (trashedAt desc, id desc)
/keys, /grants (createdAt desc, id desc)
/webhooks/{id}/deliveries (createdAt desc), and within one second the newest first
/search by kind (to-dos, then containers, then notes, then capture rows, then attachments), then id

sortKey is the order the apps show, a stored key rather than a timestamp. /search has no relevance score to sort by: the kind grouping is the engine's emission order, and id is what makes it total.

?updated_since= filters, it never orders. It is applied before the sort, so a filtered page still comes back in the list's own order. There is no way to ask for rows in the order they changed.

Next