# Keeping a copy in sync

plnnrly has no webhooks. Poll each collection from a high-water mark on updated_at, re-check cheaply, and catch deleted records.

plnnrly does not push changes to you: there are no webhooks. To keep your own copy of the data up to date, you poll. This page shows a polling loop that does not miss changes and stays well inside your rate budget.

In the examples, `<your-url>` is the address you sign in to plnnrly on, such as `acme.plnnrly.com`, or your organisation's own domain.

## The high-water mark

Every collection accepts the filter `filter[updated_at][gte]` and the sort key `updated_at`. That is all a sync needs:

1. Read the collection sorted by `updated_at`, starting 60 seconds before your stored mark.
2. Follow `links.next` until it is `null`.
3. Insert or update each row in your copy by its `id`.
4. Store the highest `updated_at` you processed as your new mark.

```bash
curl -G https://<your-url>/api/v2/projects \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" \
  --data-urlencode "filter[updated_at][gte]=2026-10-15T09:00:00Z" \
  --data-urlencode "sort=updated_at,id" \
  --data-urlencode "per_page=100"
```

`links.next` in each response is the full URL of the next page, with your filter and sort carried over. Request it as it is:

```bash
curl "<links.next from the previous response>" \
  -H "Authorization: Bearer <token>"
```

For the very first run, leave the filter out and read the whole collection the same way.

Things to know:

- **Overlap is intended.** Start each run a little before your stored mark, for example 60 seconds before it. A change is stamped when it is made, not when it is saved for good. It can therefore appear after a read that has already passed its timestamp, and the overlap picks it up. Updating your copy by `id` makes the repeats harmless.
- **Use the exact format.** A timestamp in a filter is UTC with a `Z` and no fractions of a second, such as `2026-10-15T09:00:00Z`. See [Data formats](https://developers.plnnrly.com/en/docs/api/v2/guides/data-formats/).
- **A row can appear twice in one run.** When a row changes while you are paging, it moves to the end of the sort order and comes back later in the same run.
- **Related lists are collections of their own.** `updated_at` belongs to the row itself. Poll lists that belong to a record, such as a work item's comments, separately.
- **Some collections default to your own rows.** `/worklogs`, for example, returns only your own entries unless you name other users in `filter[user_id]`, which needs an extra permission. The operation page says so. See [Filtering and sorting](https://developers.plnnrly.com/en/docs/api/v2/guides/filtering-and-sorting/).

## Cheap re-checks

Small lists that you re-read in full, such as labels or workflow statuses, have a cheaper check. Send the page's weak `ETag` back as `If-None-Match`:

```bash
curl -i https://<your-url>/api/v2/labels \
  -H "Authorization: Bearer <token>" \
  -H 'If-None-Match: W/"a1b2c3d4e5f6"'
```

An unchanged page answers `304 Not Modified` with no body, and costs 1 unit. A single record works the same way with its own `ETag`. See [Concurrency and ETags](https://developers.plnnrly.com/en/docs/api/v2/guides/concurrency-and-etags/#etag).

## Deletions

Records disappear in two ways. [Deleting and restoring](https://developers.plnnrly.com/en/docs/api/v2/guides/deleting-and-restoring/) lists which resources do what.

**Archived records.** On collections whose rows carry `deleted_at`, a delete archives the record. Add `trashed=with` to your sync query. Archiving and restoring both move `updated_at`, so the change arrives through your high-water mark, with `deleted_at` set or cleared.

**Permanent deletes leave no trace to poll for.** That includes a project deleted with `force=true`. Find them with a periodic id scan: read the whole collection by `id`, and remove from your copy every row the scan did not return.

```bash
curl -G https://<your-url>/api/v2/projects \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "sort=id" \
  --data-urlencode "trashed=with" \
  --data-urlencode "per_page=100" \
  --data-urlencode "view=summary"
```

Keep `trashed=with` in the scan on collections that archive, or archived rows look deleted. A row you are no longer allowed to see also drops out of the scan.

## What it costs

Take a sync of projects, customers and work items every 5 minutes, plus a nightly id scan. Costs follow [Rate limits](https://developers.plnnrly.com/en/docs/api/v2/guides/rate-limits/):

| Step                                                                                                                      | Requests | Units |
| ------------------------------------------------------------------------------------------------------------------------- | -------- | ----- |
| A run where nothing changed: one first page per collection                                                                | 3        | 3     |
| A run that finds 250 changed work items: three pages of up to 100 rows, plus one page each for projects and customers     | 5        | 5     |
| The same run with `include=labels` on the work items: a to-many path limits the page to 25 rows and adds 3 units per page | 12       | 42    |
| A nightly id scan of 12,000 work items, at 100 per page                                                                   | 120      | 120   |

Every step stays far below the 600 read units a token may spend per minute. Keep includes out of the sync loop unless you need them: they cost more than they look.

A separate read-only token for the sync gives it its own per-token budget. The organisation's budget is still shared with every other token. See [API tokens](https://developers.plnnrly.com/en/docs/api/v2/guides/tokens/).

## Do not sync over MCP

The MCP server is made for an AI assistant answering questions. It allows 60 requests per minute per user, lists default to 10 rows, and an answer is cut off at 30,000 characters: see [Tools](https://developers.plnnrly.com/en/docs/mcp/tools/). Sync with the REST API and a token instead.

## Related

- [Pagination](https://developers.plnnrly.com/en/docs/api/v2/guides/pagination/)
- [Deleting and restoring](https://developers.plnnrly.com/en/docs/api/v2/guides/deleting-and-restoring/)
- [Rate limits](https://developers.plnnrly.com/en/docs/api/v2/guides/rate-limits/)
- [What the API cannot do](https://developers.plnnrly.com/en/docs/api/v2/guides/known-limitations/)
