Skip to content
PicoRank Docs
Website Open app

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_gui with 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_at in 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".