Skip to content
PicoRank Docs
Website Open app

Conventions

Pagination, idempotency, concurrency, rate limits and request ids — the mechanics that apply everywhere.

Envelopes

A single resource comes back under data:

{ "data": { "id": "…", "host": "example.com" } }

A collection adds pagination fields:

{ "data": [ … ], "has_more": true, "next_cursor": "eyJ2Ijoi…" }

Some reads also carry a notice — a fact about the data rather than an error. See the catalogue.

Pagination

Cursor-based. Pass limit (default 50, max 200) and follow next_cursor verbatim until has_more is false.

curl "https://api.picorank.com/v1/keywords?limit=100" -H "Authorization: Bearer $KEY"
curl "https://api.picorank.com/v1/keywords?limit=100&cursor=eyJ2Ijoi…" -H "Authorization: Bearer $KEY"

Cursors rather than offsets because the underlying tables are written continuously by the scanning worker: with ?page=2, rows inserted between your requests would silently shift the window and you would skip or repeat results. A cursor encodes a position, so it stays correct under concurrent writes.

A cursor is bound to the query that produced it. Replaying one against different filters returns invalid_cursor rather than a plausible-looking wrong page.

Idempotency

Every POST accepts an Idempotency-Key header. Retrying with the same key returns the original response instead of performing the work twice, for 24 hours.

curl -X POST https://api.picorank.com/v1/jobs \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: 3f9a1c2e-…" \
  -H "Content-Type: application/json" \
  -d '{"kind":"rank_run"}'

Use it on anything that spends. A request that times out on the network may well have succeeded on ours; without an idempotency key, your retry buys the data a second time.

Reusing a key with a different body is reported as idempotency_mismatch — a mistake worth surfacing rather than silently returning someone else's answer.

Concurrent edits

There is no optimistic-concurrency control yet. Updates are last-write-wins: if two clients PATCH the same resource, the second overwrites the first without warning.

In practice this API is written to by one integration at a time, so the risk is small — but it is a real gap and we would rather say so than let you discover it. ETag / If-Match support is planned; when it lands, a stale update will be refused rather than silently applied.

Rate limits

Per key, per class of endpoint:

ClassLimitWindow
Reads60060s
Writes12060s
Expensive (anything that can start paid work)2060s

Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. A 429 carries Retry-After. Honour it rather than retrying on a fixed timer.

Rate limits are about traffic; they are not the spending control. A client comfortably inside its request budget can still be out of quota — and the error will say which. See Spending and quotas.

Request ids

Every response carries X-Request-Id, echoed in error bodies as request_id. Log it. You can also send your own X-Request-Id and we will use it, which makes correlating your logs with ours trivial.

CORS

The API accepts cross-origin requests. That is safe because it authenticates with bearer keys and never cookies — there is no ambient authority for another site to borrow. It does not mean keys belong in a browser: anything you ship to a client is public. The permissive policy exists so tools like the sandbox on this site work.