Authentication
Every gateway request carries a credential in the Authorization header. There
are three kinds, and which one you use is a decision about where the code runs.
The three credentials
Section titled “The three credentials”| Credential | Looks like | Where it runs |
|---|---|---|
| Secret key | psg_live_7hQ2mKpR9fL3xW8v_4bNq… |
Your servers. Never anywhere else. |
| Publishable key | psg_pk_live_7hQ2mKpR9fL3xW8v |
A browser or mobile app. Restricted to client-safe scopes. |
| Client token | psg_ct_… |
A browser, for one transaction, for up to an hour. |
Authorization: Bearer psg_live_7hQ2mKpR9fL3xW8v_4bNq…The psg_ prefix is deliberate: GitHub and GitLab run secret scanners over public
pushes and can notify a vendor whose prefix they recognise. A key that looks like
random base62 is invisible to them.
The environment is in the key
Section titled “The environment is in the key”Every key says live or test in the string itself, not just in our database.
psg_test_7hQ2mKpR9fL3xW8v_… sandboxpsg_live_7hQ2mKpR9fL3xW8v_… real moneyThis exists because a test key in a live deployment — or, worse, the reverse — is
the most common integration failure there is, and it is cheap to make visible. You
can tell which environment a key belongs to by looking at it, before you paste it
into a config file. Presenting a key against the wrong host returns 403 with
code environment_mismatch, which has its own code for exactly this reason.
Secret versus publishable
Section titled “Secret versus publishable”A secret key authenticates as your account and can do anything its scopes allow. Treat it like a database password: server-side environment or secret manager, never in a repository, never in a mobile binary, never in frontend JavaScript.
A publishable key is designed to be visible. It is restricted at the database level to client-safe scopes, so it cannot create a payout or read your settlements even if someone lifts it out of your page source. Use it for the narrow set of things a customer’s browser legitimately needs to do — creating a payment session, confirming a checkout.
A client token is narrower still: minted for one transaction, expiring within the hour. It is what a hosted checkout page holds.
Scopes
Section titled “Scopes”A key carries an explicit list of scopes, and each operation requires one. You set them when you create the key in the portal. Grant the narrowest set that works: a key that can only create collections cannot be used to drain your balance if it leaks.
| Scope | Allows |
|---|---|
collection.create |
Create charges |
collection.read |
Read charges |
transaction.read |
Read transactions |
payout.create |
Create payouts |
payout.read |
Read payouts |
refund.create |
Create refunds |
refund.read |
Read refunds |
balance.read |
Read balances |
settlement.read |
Read settlements and withdrawals |
customer.read / customer.write |
Read and manage customers |
webhook.read / webhook.write |
Read events and deliveries; manage endpoints |
reference.read |
Read the reference lists (banks, networks, currencies) |
verification.create |
Start a verification |
payment_session.create / .read / .confirm |
Payment sessions (the client-safe set) |
Matching is exact. There are no wildcards and no prefix matching: holding
transaction.read does not imply transaction.reversed, and a scope ending in
.read grants nothing else. Authorisation bugs come from prefix checks, so there
are none.
A request with a valid key that lacks the required scope returns 403 with code
insufficient_scope — distinct from unauthenticated, so you can tell “wrong
key” from “right key, wrong permissions” without guessing.
Every operation’s required scope is shown in the API reference.
IP allowlisting
Section titled “IP allowlisting”A key can be restricted to a set of IP addresses or CIDR blocks. If your
integration calls from fixed egress addresses, turn this on — it means a leaked
key is unusable from anywhere else. A request from an address not on the list
returns 403 with code ip_not_allowed.
Rotation, and the grace period you should watch for
Section titled “Rotation, and the grace period you should watch for”When you rotate a key, the old one keeps working for a grace period so you can redeploy without an outage. While that is true, every response authenticated by the superseded key carries:
PaySigna-Key-Grace-Expires: 2026-10-12T09:00:00ZWatch for that header and alert on it. Without it, a rotation’s grace period is invisible to the only party who can act on it: the requests succeed, you have no way to tell which of your servers is still presenting the old credential, and you find out when every one of them fails at the same instant.
So: rotate, deploy, then check your logs for that header. If it is still appearing, something is still holding the old key.
After the grace period the old key returns 403 with code api_key_revoked —
deliberately distinct from unauthenticated, because a merchant whose key was
revoked needs to know to rotate, not to re-check their spelling.
The authentication errors, and what each one means
Section titled “The authentication errors, and what each one means”| Code | Status | What actually happened |
|---|---|---|
unauthenticated |
401 | No credential, or one we could not parse. Also returned for a wrong secret — indistinguishable on purpose, so the endpoint cannot be used to confirm that a key id exists. |
api_key_revoked |
403 | The key was valid once. Rotate. |
api_key_expired |
403 | The key had an expiry and it has passed. |
ip_not_allowed |
403 | This key requires an allowlist and your source address is not on it. |
insufficient_scope |
403 | Right key, wrong scopes. |
environment_mismatch |
403 | A test key against live, or the reverse. |
forbidden |
403 | Authenticated and in scope, but not permitted on this particular resource — usually another merchant’s. |
Sessions, for the other two APIs
Section titled “Sessions, for the other two APIs”The merchant portal and provider console APIs authenticate with a PASETO session rather than a key. Refresh tokens rotate on every use, and presenting a rotated generation revokes the whole chain — so a stolen refresh token is usable once and then kills the session it came from.
You will not implement this: it is what the portal does on behalf of a signed-in user. It is documented because those two APIs appear in the reference and their authentication model differs from this one.

