Documentation menu

working with the api

Paging, projections and safe retries

Four mechanics that decide whether an integration is correct under load: how you page, how much you ask for, how you retry, and how you avoid overwriting someone else's edit.

Cursor pagination#

The shape of data on every list endpoint. Paging is cursor based rather than offset based, 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 it or construct one; its encoding is not part of this contract and will change.

The first page

returns 200
curl
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
curl
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, on purpose. Counting a growing table on every list request is the kind of query that gets slower exactly as a customer succeeds. Page until next_cursor is null.

Page size. Values above the maximum are clamped rather than rejected, so a client asking for a thousand rows gets a hundred and a next_cursor.

Incremental sync#

To keep a copy of your content in step without re-reading everything, store the greatest updated_at you have seen and pass it back as updated_since. Combine it with the default ordering, updated_at.desc, 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 are the better answer when you want to react quickly. Polling is the right answer when you want to be sure you have everything, including anything a delivery failure lost. Most integrations do both.

Sparse fieldsets#

List endpoints omit article bodies by default. A list endpoint that returns every body is the classic way to make a content API slow and expensive, so content is something you ask for one article at a time, or explicitly with a small limit.

Comma separated field allow list. Any field of the Article schema may be named. Omit for the default projection, which is: id, status, title, slug, excerpt, featured_image_url, category_id, author_id, format_id, published_at, scheduled_publish_at, created_at, updated_at. id is always returned whether or not you name it.

fields is an allow list, not a wildcard. There is no way to ask for every field, and id is always returned whether or not you name it.

Idempotency-Key#

A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice.

Retry with the same key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out.

Same key with a different body is 409 IDEMPOTENCY_KEY_CONFLICT, because reusing a key for different work is a bug in your client rather than a retry. Same key while the first request is still running is 409 IDEMPOTENCY_KEY_IN_FLIGHT; wait and retry.

Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation.

Two responses deliberately do not replay: the secret from a key creation, and the presigned URL from an upload reservation. Both are live credentials, and persisting one so that a retry could see it again would put it in every database backup taken during the replay window. A replay returns the same object id with the credential field null and a *_replayable: false flag, so you can tell the difference between a redacted replay and a missing field.

If-Match and lost updates#

The ETag from your last read of this object. If it has changed since then you get 412 PRECONDITION_FAILED and your write is not applied, so two people editing the same article cannot silently overwrite each other.

Optional in v1 for compatibility. Omitting it means last write wins. Send it.

A safe update with If-Match

returns 200
curl
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, and deliberately: it versions the row rather than the bytes, so a fields projection legitimately returns a different body for the same version.