Skip to content

Authentication and keys

Every request carries the key as a bearer token:

Authorization: Bearer sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • Keys are sk_live_ or sk_test_ followed by 32 characters.
  • Server-side only. The API has no CORS and will not answer a browser. A key in front-end code is a key in every visitor's developer tools.
  • Never put the key in a query string. It is not accepted there, and it would land in access logs and Referer headers.
  • PamoPay stores only a hash of the key. It is shown once, when made, and nobody — including us — can read it back. A lost key is replaced, not recovered.

Scopes

Every key carries a list of what it may reach, chosen when it is made and never changed afterwards. One scope per resource and verb:

ScopeLets the key
charges:readGET /v1/charges, GET /v1/charges/{id}
charges:writePOST /v1/charges, POST /v1/pos/charges
balance:readGET /v1/balance
bills:readGET /v1/bills, GET /v1/bills/{id}
bills:writePOST /v1/bills, POST /v1/bills/{id}/notify
links:readGET /v1/payment_links, GET /v1/payment_links/{id}
links:writePOST /v1/payment_links
events:readGET /v1/events, GET /v1/events/{id}
events:writePOST /v1/events/{id}/retry
webhooks:readGET /v1/webhook_endpoints
webhooks:writePOST /v1/webhook_endpoints, DELETE /v1/webhook_endpoints/{id} — where events go. Keep it off the key that takes payments.

A call outside the key's scopes is refused with FORBIDDEN_SCOPE (403), and the body names what was needed:

json
{ "error": { "code": "FORBIDDEN_SCOPE", "scope": "charges:write", "doc": "…", "request_id": "…" } }

Give each server the narrowest key it needs. A finance script that reads events does not need charges:write, and a leak of it then costs nothing. The dashboard's default for a new key is every read plus charges:write — enough for a checkout — and everything else is a tick. Keys made before scopes existed hold every scope.

There is no wildcard. A key that should reach everything names everything, so a resource added later is one no existing key can reach until somebody says so.

Scopes never widen and never narrow. To change what a key may do, make a new one. Replace on the dashboard does this in one step: a new key with the same scopes, label and lifetime, and the old one retiring in 24 hours.

Lifetime

A key may be given a lifetime when it is made — 30 days, 90 days, a year, or none. A key past its lifetime is refused with KEY_EXPIRED (401). Lifetimes cannot be extended; make a new key. A contractor's key should have one. A production server's usually should not — rotate it on your own schedule instead. Keys made before lifetimes existed never expire.

Rotation

Retiring a key has two speeds, and they are not the same action.

In 24 hours is the rotation. Make the new key (or press Replace), deploy it at your own pace, and the old one stops when the grace runs out. During the grace both keys work. Never an outage.

Now is the compromise. The key stops on the next request. Anything still using it fails, which is the correct trade when the alternative is somebody else taking payments as you. A retired key is answered with KEY_REVOKED.

A rotation runbook, for a production server:

  1. On the Developers screen, press Replace it on the key. Copy the new one.
  2. Put it in your secret store. Deploy.
  3. Watch Used … ago on the old key's row. When it stops moving, it is done — and it stops on its own within 24 hours anyway.

If a key leaks

  1. Press Stop it now on that key. Do it first; it costs nothing and reverses nothing.
  2. Make a new key with the same scopes and deploy it.
  3. If the key was in a repository with your webhook secret, point your endpoint at the same URL and save — no: point it somewhere new, which rotates the secret. Saving an unchanged URL keeps it.
  4. Read GET /v1/charges and GET /v1/events for the window it was exposed. Every charge under a key is yours to see; every refusal was logged with a request_id.

Keys are sk_live_ and sk_test_ plus 32 base62 characters, which is the pattern secret-scanning tools look for. Committing one to a public repository is the commonest way a key leaks, and the cheapest to prevent.

Refusals

Code
BAD_KEY401Missing, malformed or not one we issued.
KEY_REVOKED401Retired, and past its grace.
KEY_EXPIRED401Reached its lifetime.
FORBIDDEN_SCOPE403Real key, wrong scope. scope says which.
MERCHANT_NOT_LIVE403Live key on a business that is not live. About the account, not the key.

Who may make a key

Only the owner and developer seats on the dashboard, and only from a session that signed in within the last fifteen minutes. A developer seat holds keys, endpoints and the event log — an integration — and deliberately cannot see the balance, because the person wiring up a checkout is very often a contractor.

Every code, scope and route on this site is rendered from the API's own source.