Authentication
API keys: how to create them, what they can reach, and how read-only is enforced.
Keys
Every request carries an API key as a bearer token. Create keys in the app under Settings → API keys. Only account owners and administrators can create them.
curl https://api.picorank.com/v1/domains \
-H "Authorization: Bearer pr_live_A1B2C3D4E5F6_…" A key looks like this:
pr_live_A1B2C3D4E5F6_KX7QW2M9ZR4T8YB6NC3VF5HJ1PDS0GLA
│ │ │ └─ secret — 160 bits, shown once, stored only as a hash
│ │ └────────────── public identifier, used to find the key
│ └─────────────────── environment: live or test
└────────────────────── vendor prefix The secret is shown once, at creation. We store only its SHA-256 hash, so we genuinely cannot recover it — not as a policy, but as a fact about what is in the database. If a key is lost, revoke it and create another.
Environments
A pr_test_ key works against api.staging.picorank.com and a pr_live_ key
against api.picorank.com. Using one against the other returns
key_wrong_environment rather than a bare 401, because
"your key is fine, you pointed it at the wrong host" is a five-second fix and an unexplained 401 is an afternoon.
What a key can reach
Two independent choices are made when a key is created.
Access: the whole account, or one domain
An account-wide key sees every domain, plus account-level data — usage, entitlements, members, billing state.
A domain-scoped key sees exactly one domain and nothing else. It cannot list your other domains,
cannot read account-level resources (it gets
scope_insufficient), and cannot delete the domain it is
scoped to. This is the key to hand to a contractor working on one site, or to embed in a per-client dashboard.
Domain scoping is enforced in the database, not only in the API. Every table that carries a domain has a policy requiring the key's domain to match, so a bug in our own code cannot leak another domain's rows — the query simply returns nothing.
Permission: read, or read and write
A read key can retrieve anything in its scope and change nothing. A write key can also create, update and delete.
Read-only is enforced twice. The API refuses the request with
permission_read_only and a sentence explaining it; and
underneath, a read key runs as a database role that holds no write privileges at all. The second layer is the real
boundary — the first exists so the message is useful.
What no key can do
Some things are deliberately unreachable from the API at any scope:
- Managing API keys. A leaked key cannot mint a broader one, extend its own expiry, or read the audit trail showing what it did.
- Changing account credentials — password, email, accepting invitations.
- Deleting the company.
- Changing a subscription. Billing is readable; buying a plan is a Stripe checkout flow that needs a
human. You get
billing_requires_guiwith a link, rather than an endpoint that pretends to work.
Keeping keys safe
- Set an expiry when you create a key. A key that lapses is one fewer thing to remember.
- Scope narrowly. Most integrations read one domain; give them a read key for that domain and nothing more.
- Check
last_used_atin the app. A key nobody has used in months should be revoked. - Revocation is immediate. Authentication reads the key row on every request, so there is no cache to wait out.
- Keys belong on servers. Anything you ship to a browser is public. If you need per-user access in a front end, proxy through your own backend.
-
The
pr_live_prefix is designed to be recognisable by secret scanners — if one of ours turns up in a public repository, the scanner should catch it.
The audit trail
Every change a key makes, and every request it was refused, is recorded with the key, the operation, the result and
the request id, visible under Settings → API keys. Reads are not logged individually — the volume would bury the
signal, and last_used_at already answers "is this key still in use".