Skip to content
Loading keys…

Authentication

Key-based auth: three key types, scoped permissions, IP allow-lists.

Wajub authenticates with an API key sent raw in the Authorization header. There is no Bearer prefix, no OAuth dance, no signing step.

Request headers
Authorization: sk_test.xxxxxxxxxxxxxxxx...
Content-Type: application/json

Anatomy of a key

A key is a prefix, a dot, then a random secret. The prefix is the part you read:

Key format
sk_test . xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
└──┬──┘   └──────────────────┬──────────────────┘
 │                         └─ random secret, shown once at creation
 └─ type (sk/pk/rk) and environment (_test or nothing)

The prefix carries two facts at once: the key's type, and its environment.

PrefixTypeEnvironment
pk.PublicLive
pk_test.PublicSandbox
sk.PrivateLive
sk_test.PrivateSandbox
rk.RestrictedLive
rk_test.RestrictedSandbox

The key picks the environment, not the URL

https://api.wajub.com serves both environments. A _test key only ever reaches sandbox data, so the same code reaches test or live data purely by which key you load. Switching environments is a change of secret, never a change of endpoint.

The three key types

Private (sk.) grants every operation and must never leave your server. Restricted (rk.) is a private key narrowed to an explicit list of scopes, for a third party or a single integration. Public (pk.) is the key you ship to a browser or a mobile app to create a payment, and it reaches only what a checkout needs:

  • creating a payment (POST /payments), charging it or one of its instalments and finishing the payer's step (POST /payments/{id}, /splits and /confirm), and reading it by id (GET /payments/{id});
  • the ways to pay (GET /payments/options), and creating a customer (POST /customers);
  • the reference data: GET /, /channels, /countries, /currencies and /providers.

Every other route refuses it with 406 Private Key Required: listing payments, cancelling, activating, updating or closing one and listing its refunds; listing, reading or changing customers; links and invoices; and anything that moves money out, reads your balance or manages your account, a customer's tax_ids included. In sandbox, links and invoices, being live only, answer 403 first. Whoever reads your page can use what a public key reaches, so holding it is not a permission: never use it as access control.

What a public key reads back is narrowed the same way. On POST /payments, customer, shipping_address and billing_address hold only what the request sent, never the customer your account already has with that email or phone. Read with GET /payments/{id}, or returned by POST /payments/{id}, /splits or /confirm, a payment has no customer, shipping_address or billing_address, and its payment_method holds only channel, without account, the payer's number or card. On POST /customers, a customer your account already has with that email or phone comes back as its id and the fields the request sent, never what your account holds. Read them from your server, with a secret or restricted key.

Which endpoints accept which type:

EndpointsPublic pk.Private sk.Restricted rk.
GET /, /channels, /countries, /currenciesYesYesYes
GET /providers, GET /payments/optionsYesYesWith the scope
POST /payments, GET /payments/{id}, POST /payments/{id}, /payments/{id}/splits, /payments/{id}/confirmYesYesWith the scope
POST /customersYesYesWith the scope
The rest of /payments and /customers, /links, /invoicesNoYesWith the scope
/balance, /transfers, /refunds, /beneficiaries, /disputes, /webhooks, /events, /identity, /accountsNoYesWith the scope
/taxNoYesWith the scope
/shieldNoYesNo

A public key sent to a row marked No is refused before the call does anything:

Response · 406 Not Acceptable
{
"code": 406,
"status": "Not Acceptable",
"message": "Private Key Required"
}

Restricted key scopes

A restricted key carries a list of resource.action scopes. GET, HEAD and OPTIONS calls need read, everything else needs write. A GET /transfers therefore needs transfer.read, a POST /transfers needs transfer.write.

Scope pairGrants access to
payment.read / payment.write/payments, and /providers
customer.read / customer.write/customers
transfer.read / transfer.write/transfers
refund.read / refund.write/refunds, and the refunds of a payment: GET /payments/{id}/refunds takes refund.read as well as payment.read, and POST /payments/{id}/close with outcome: refund takes refund.write as well as payment.write
recipient.read / recipient.write/beneficiaries
invoice.read / invoice.write/invoices
dispute.read / dispute.write/disputes
webhook.read / webhook.write/webhooks
event.read / event.write/events, including resending a delivery
link.read / link.write/links
identity.read / identity.write/identity
tax.read / tax.write/tax
balance.read/balance, no write endpoint exists
account.read/accounts, read only

Two scopes in the list have no writable counterpart. balance.read is read-only because the balance has no write endpoint. /accounts is a different case: creating, updating or deleting a subaccount is a write, and no account.write scope exists yet, so those three calls need a private key. The same applies to /shield, which has no scope at all and is therefore private-key only.

Four paths ignore scopes entirely, because they return the same data for every key on the platform: GET /, /channels, /countries and /currencies.

A scope you did not grant returns a 403 naming exactly what was missing:

Response · 403 Forbidden
{
"code": 403,
"status": "Forbidden",
"message": "This API key does not have permission to write transfer"
}

Narrowing a key further

Two optional constraints apply to any key type, set when you create it in Settings → Developers → API keys. You can change them later from the same page, on every key type, and editing anything else about the key leaves them as they were. The expiry is shown next to the key.

allowed_ipsarrayoptional
Exact IPv4/IPv6 addresses or CIDR ranges. A request from anywhere else is refused with 403 IP address not allowed for this API key, whatever the key type.
expires_attimestampoptional
After this moment the key answers 401 Invalid or revoked API credentials. Leave it empty for a key with no end date.

Authenticating a request

The same call in the five supported stacks. GET /balance needs a private key, which makes it a useful check that your server-side credential is loaded:

curl https://api.wajub.com/balance \
-H "Authorization: $WAJUB_API_KEY"

Or send it straight from this page. Sign in from the top bar first, the request then goes out with your sandbox key:

Authentication errors

Four different failures, all distinguishable by the message:

CodemessageWhat happened
401Invalid API credentialsNo key, or no account matches it.
401Invalid or revoked API credentialsThe key existed but is inactive, deleted or expired.
403IP address not allowed for this API keyThe key has an allow-list and you are not on it.
406Private Key RequiredA public key on a private-only endpoint.
Response · 401 Unauthorized
{
"code": 401,
"status": "Unauthorized",
"message": "Invalid API credentials"
}

Private keys are blocked in the browser

Never use a private key (sk. or sk_test.) from a browser or any front-end code. When Wajub detects a private key used from a browser, it refuses the request and alerts you: the secret has reached the front end.

Response · 403 Forbidden
{
"code": 403,
"status": "Forbidden",
"message": "Security Alert: You are trying to use a Private Key from a browser/frontend. Private keys must ONLY be used in server-side code. Please use a Public Key for frontend requests.",
"documentation_url": "https://docs.wajub.com/api/authentication#private-keys-are-blocked-in-the-browser"
}

When this happens, the account owner and admins get a security alert by email. If you hit it unexpectedly, look for a private key bundled into front-end code, then either move the call to your server or switch it to a public key.

A restricted key (rk. or rk_test.) belongs on a server too. When Wajub detects one used from a browser, the account owner and admins get the same security alert. For now the request still goes through; Wajub will refuse it with the same 403 later, so move the call to your server now and replace the key.

Practices worth keeping

  • Keep keys in environment variables, never in the repository and never in a front-end bundle.
  • Use a distinct key per service, not one key shared across your whole estate: a leak or a revocation then touches one service. It buys no throughput: the rate limit is counted for the whole account, whatever key the call carries.
  • Give third parties a restricted key with the two or three scopes they actually need.
  • Set expires_at on anything temporary: a migration script, an agency's access, a prototype.
  • Rotate after anyone with access to your keys leaves.

What did you think of this content?