---
title: "Paging, projections and safe retries"
description: "Cursor paging, sparse fieldsets, Idempotency-Key and If-Match."
canonical: "https://writavo.com/docs/pagination"
last-updated: "2026-08-07"
---

# Paging, projections and safe retries

How to page, ask only for the fields you need, retry safely, and avoid overwriting someone else's edit.

## Cursor pagination

The shape of `data` on every list endpoint. Paging is by cursor, so rows are never skipped or
repeated when content changes mid pagination.

The opaque cursor from `data.next_cursor` on the previous page. Do not parse or build one; its
encoding will change.

**The first page** (returns 200)

```bash

curl https://api.writavo.com/v1/articles?limit=2&fields=id,title,status,updated_at \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"

```

Cursor paging, so rows are never skipped or repeated when content changes mid pagination.

**The next page** (returns 200)

```bash

curl https://api.writavo.com/v1/articles?limit=2&fields=id,title,status,updated_at&cursor=eyJ2IjoxLCJrIjpbIjIwMjYtMDgtMDZUMTA6MDA6MDBaIl19 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"

```

Pass back `data.next_cursor` verbatim. Do not parse it or construct one.

> There is no total count. Page until `next_cursor` is `null`.

Page size. Values above the maximum are clamped, not rejected: ask for a thousand and you get a
hundred and a `next_cursor`.

## Incremental sync

To stay in step without re-reading everything, store the greatest `updated_at` you have seen, pass it back as `updated_since`, and page until the cursor runs out.

pattern:

```
# first run
GET /articles?limit=100&order=updated_at.asc

# every run after that
GET /articles?limit=100&order=updated_at.asc&updated_since=2026-08-06T09:41:12Z
```

Webhooks react faster; polling makes sure you have everything, including anything a failed delivery lost. Most integrations use both.

## Sparse fieldsets

List endpoints leave out article bodies by default. Ask for `content` one article at a time, or explicitly with a small `limit`.

> `fields` is an allow list with no wildcard. `id` is always returned.

## Idempotency-Key

A unique key per logical operation, 1 to 255 printable ASCII characters (a UUID works), scoped
to the Site and endpoint and kept for 24 hours.

- Same key and body: the original response is replayed, so retrying a timeout is safe.
- Same key, different body: `409 IDEMPOTENCY_KEY_CONFLICT`.
- Same key while the first is running: `409 IDEMPOTENCY_KEY_IN_FLIGHT`; wait and retry.

> Two responses do **not** replay, because they are live credentials: the secret from a key creation and the presigned URL from an upload reservation. A replay returns the same object id with the credential field `null` and a `*_replayable: false` flag.

## If-Match and lost updates

The `ETag` from your last read. If the object changed since, you get `412 PRECONDITION_FAILED`
and nothing is written. Optional in v1; without it the last write wins. Send it.

**A safe update with If-Match** (returns 200)

```bash

curl -X PATCH https://api.writavo.com/v1/articles/3f1b0c7a-0000-4000-8000-000000000001 \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -H "If-Match: W/"1767225600000"" \
  -d '{
  "excerpt": "A practical framework for picking a headless CMS."
}'

```

Send the ETag from your last read. If someone else changed the article you get 412 instead of silently overwriting their work.

- Read the object, keep the `ETag`.
- Send it back as `If-Match` on your update.
- On `412 PRECONDITION_FAILED`, re-read, merge, and retry with the new `ETag`. Nothing was written.

The tag is weak: it versions the row, not the bytes, so a `fields` projection can return a different body for the same version.
