Skip to content

Keep a copy in sync

The change feed lists every new item, change and removal in order. Read it with a cursor and you get each change exactly once.

The change feed (/changes) is the way to keep your own copy of a dataset current: instead of reading everything again, you read what happened since last time. It is in the paid plans; on Free it answers 402.

How it works

Every change has an id, and ids only grow. You ask for the changes after the last id you have; the answer gives you the next ones, oldest first, and the cursor to continue from:

Shell
curl -H "Authorization: Bearer $PRICANA_KEY" "https://pricana.io/api/v1/changes?after=0&limit=1000"
JSON
{
  "changes": [
    {
      "id": 812,
      "kind": "CHANGED",
      "observedAt": "2026-10-06T21:03:00Z",
      "price": 1399.0,
      "previousPrice": 1499.0,
      "changes": { "price": [1499.0, 1399.0] },
      "item": { "id": 77, "title": "Lenovo ThinkPad X1 Carbon Gen 12", "url": "https://…", "currency": "EUR" }
    }
  ],
  "nextCursor": 812,
  "hasMore": true
}
  1. Start with after=0 (the beginning of your plan's history), or with a cursor you stored.
  2. Process the changes. Then pass nextCursor as after in the next request.
  3. While hasMore is true, ask again at once. When it is false, you are up to date: store the cursor and ask again later (every few minutes is plenty).

Read like this, you get each change exactly once, in order, also after a pause: the feed keeps your plan's history (e.g. 365 days on Pro). Changes appear in the feed about 30 seconds after they happen.

A complete sync loop

Python
import os, time, requests

KEY = os.environ["PRICANA_KEY"]
BASE = "https://pricana.io/api/v1"


def load_cursor() -> int:
    try:
        return int(open("pricana.cursor").read())
    except FileNotFoundError:
        return 0


def save_cursor(cursor: int) -> None:
    with open("pricana.cursor", "w") as f:
        f.write(str(cursor))


cursor = load_cursor()
while True:
    r = requests.get(f"{BASE}/changes", params={"after": cursor, "limit": 1000},
                     headers={"Authorization": f"Bearer {KEY}"}, timeout=60)
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", "5")))
        continue
    r.raise_for_status()
    page = r.json()
    for change in page["changes"]:
        apply(change)          # your code: insert, update or mark removed
    cursor = page["nextCursor"]
    save_cursor(cursor)        # after applying: a crash repeats changes, never skips them
    if not page["hasMore"]:
        time.sleep(300)

Store the cursor after you applied the changes, in the same transaction if you can. Then a crash at worst repeats some changes, and applying a change twice does no harm if you key your rows by item id.

Applying changes

kindin your copy
NEWinsert the item (read it with /items/{id} if you need all its fields)
CHANGEDupdate it: changes has [old, new] for every field that changed; price is the new price
REMOVEDmark it as no longer listed
RELISTEDmark it as listed again

Changes carry the item's id, title, link and site. For the full item, read /items/{id}, or read many at once with /items?changedSince= (below).

The first copy

The feed starts at the beginning of your plan's history, which may be long. To start fresh instead:

  1. Read /changes/recent?limit=1 and keep the id of the change in it (with none, keep 0). A change shows there when it shows in the feed, so nothing after it is missed.
  2. Then read all items with an export (or /items page by page).
  3. Read the feed from after=<that id>. Changes made during the export come once more; applying a change again does no harm.

Simpler and often enough: /items?changedSince=<time of last sync>&status=all returns every item that changed since then, in its current state. Removed items are among them with removedAt set: delete them from your copy. (Without status=all you get listed items only, and a copy kept this way never learns of removals.)

One dataset only

dataset=<id> narrows the feed to one of your datasets. Keep one cursor per feed you read.

Pushed instead of polled

Webhooks send the same changes to your server as they happen, in the same order, with the same objects. Use them when you want changes pushed within a minute, the feed when you prefer to pull.