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:
| Class | Limit | Window |
|---|---|---|
| Reads | 600 | 60s |
| Writes | 120 | 60s |
| Expensive (anything that can start paid work) | 20 | 60s |
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.