Every collection read returns one page of rows at a time. This page shows how to read the page state, move between pages, and walk a whole collection.
Paths are relative to https://<your-url>/api/v2, where <your-url> is the address you sign in to plnnrly on, such as acme.plnnrly.com, or your organisation's own domain.
The envelope
A collection answers with data, meta.page and links. The first page of GET /projects:
{
"data": [
// 25 projects in the summary view
],
"meta": {
"page": {
"per_page": 25,
"next_cursor": "<opaque cursor>",
"prev_cursor": null,
"total": 184,
"current_page": null,
"last_page": null
}
},
"links": {
"self": "https://<your-url>/api/v2/projects?sort=title&trashed=without&per_page=25&view=summary",
"next": "https://<your-url>/api/v2/projects?sort=title&trashed=without&per_page=25&cursor=<opaque cursor>&view=summary",
"prev": null
}
}dataholds the rows.meta.pageholds the page state.current_pageandlast_pageare present on collections that support page numbers, andnulluntil you sendpage.linksholds ready-made URLs.nextandprevrepeat every parameter that shaped the page, including the defaults, so following them keeps your query intact.
meta.warnings appears next to meta.page when there is something to report. See Views and includes. Every member of the envelope is described on the pagination schema page.
Page size
per_page sets the number of rows. It defaults to 25 and goes up to 100.
When include names a relation that holds several records, such as labels on a work item, the maximum is 25. Above that you get 400 invalid-query-parameter with code out_of_range. See Views and includes.
All limits are listed together in Limits at a glance.
Cursor paging
Cursor paging is the default. Send no paging parameter and you get the first page.
To get the next page, request links.next as it is. It is already URL-encoded. You can also send the cursor yourself, as cursor; it is the same value as meta.page.next_cursor:
curl -G "https://<your-url>/api/v2/projects" \
-H "Authorization: Bearer <token>" \
--data-urlencode "cursor=<cursor from links.next>"On the last page, links.next and next_cursor are null.
A cursor is tied to the query that produced it: its sort, its filters, q, trashed and view. Change any of those and the cursor no longer fits, which is 400 invalid-cursor. Start again from the first page when you change the query. You may change per_page between pages.
Never build or edit a cursor yourself. Use a cursor to fetch the next page, not to resume a job days later. To pick up where a sync left off, filter on updated_at instead; see Keeping a copy in sync.
With page numbers, a row added to an earlier page shifts every later row by one, so you read one row twice. A cursor is not thrown off by rows added or deleted elsewhere: it continues after the last row you read. A row whose sort value changes while you page can still move past the cursor, so sort by id when you must not miss a row.
Page numbers
Some collections also accept page, counted from 1 up to 1000. They are the collections that count their rows on every request, because a page number is only useful when you know the last page:
- Work items
/work-items/{work_item} /attachments /work-items/{work_item} /links /work-items/{work_item} /subtasks - Sprints
/sprints- Time
/timers- Billing
/customer-subscriptions/customer-subscriptions/{customer_subscription} /items /invoices/sales-invoices/sales-invoices/{sales_invoice} /rows /subscription-products/subscription-types/subscription-types/{subscription_type} /items - Organization
/company-branches/users
On any other collection, page is 400 unsupported-parameter. The operation page lists page when a collection accepts it.
Send page and the response switches to page mode. GET /projects?page=2:
{
"data": [
// 25 projects in the summary view
],
"meta": {
"page": {
"per_page": 25,
"next_cursor": null,
"prev_cursor": null,
"total": 184,
"current_page": 2,
"last_page": 8
}
},
"links": {
"self": "https://<your-url>/api/v2/projects?sort=title&trashed=without&per_page=25&page=2&view=summary",
"next": "https://<your-url>/api/v2/projects?sort=title&trashed=without&per_page=25&page=3&view=summary",
"prev": "https://<your-url>/api/v2/projects?sort=title&trashed=without&per_page=25&page=1&view=summary"
}
}In page mode, links.next and links.prev are page-number links, and both cursors are null. page and cursor cannot be combined: sending both is 400 with code mutually_exclusive.
Paging past the end
links.next is null on the last page. If you ask for a page beyond last_page, you get an empty data list, not an error, and current_page repeats the number you sent. A page above 1000 is 400 with code out_of_range; use cursors to go further.
Lists people reorder
Some numbered collections are sorted by default on a position that changes whenever someone drags a row in plnnrly, such as labels, sprints, subtasks and work types. If someone reorders the list between two of your requests, page numbers can skip a row or show one twice. The page parameter on those operation pages says so.
For a walk that cannot skip rows, send sort=id and follow links.next without page.
Totals
meta.page.total is the number of rows that match your query.
- On collections that accept
page,totalis always a number. They do not acceptwith_total: sending it is400unsupported-parameter. - On every other collection,
totalisnullunless you sendwith_total=1. Counting is the expensive part of those queries, sowith_total=1costs 2 more rate units: 3 in place of 1, or 5 together withview=detail. See Rate limits.
The operation page lists page or with_total, so you can see which kind a collection is before you call it.
curl -G "https://<your-url>/api/v2/work-items" \
-H "Authorization: Bearer <token>" \
--data-urlencode "with_total=1"Walking a whole collection
Follow links.next until it is null. This exports every work item you can read, 100 at a time. It uses jq to read the JSON:
set -euo pipefail
url="https://<your-url>/api/v2/work-items?per_page=100&sort=id"
while [ -n "$url" ]; do
body=$(curl -sS --fail-with-body "$url" -H "Authorization: Bearer <token>")
echo "$body" | jq -c '.data[]'
url=$(echo "$body" | jq -r '.links.next // empty')
doneThe script stops on an error response. On a 429, wait as Retry-After says and request the same URL again; see Rate limits.
Sort by id for an export. It never changes, so a row cannot move between pages while you read. Sorting by updated_at moves a row every time someone saves it, which is what you want for a sync and not for an export.
Related
- Filtering and sorting: narrow and order the rows before you page through them.
- Keeping a copy in sync: paging by
updated_atwith a high-water mark. - Concurrency and ETags: re-read an unchanged page as a
304.