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.
Authorization: sk_test.xxxxxxxxxxxxxxxx...
Content-Type: application/jsonAnatomy of a key
A key is a prefix, a dot, then a random secret. The prefix is the part you read:
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.
| Prefix | Type | Environment |
|---|---|---|
pk. | Public | Live |
pk_test. | Public | Sandbox |
sk. | Private | Live |
sk_test. | Private | Sandbox |
rk. | Restricted | Live |
rk_test. | Restricted | Sandbox |
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},/splitsand/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,/currenciesand/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:
| Endpoints | Public pk. | Private sk. | Restricted rk. |
|---|---|---|---|
GET /, /channels, /countries, /currencies | Yes | Yes | Yes |
GET /providers, GET /payments/options | Yes | Yes | With the scope |
POST /payments, GET /payments/{id}, POST /payments/{id}, /payments/{id}/splits, /payments/{id}/confirm | Yes | Yes | With the scope |
POST /customers | Yes | Yes | With the scope |
The rest of /payments and /customers, /links, /invoices | No | Yes | With the scope |
/balance, /transfers, /refunds, /beneficiaries, /disputes, /webhooks, /events, /identity, /accounts | No | Yes | With the scope |
/tax | No | Yes | With the scope |
/shield | No | Yes | No |
A public key sent to a row marked No is refused before the call does anything:
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 pair | Grants 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:
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_ipsarrayoptional403 IP address not allowed for this API key, whatever the key type.expires_attimestampoptional401 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:
| Code | message | What happened |
|---|---|---|
401 | Invalid API credentials | No key, or no account matches it. |
401 | Invalid or revoked API credentials | The key existed but is inactive, deleted or expired. |
403 | IP address not allowed for this API key | The key has an allow-list and you are not on it. |
406 | Private Key Required | A public key on a private-only endpoint. |
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.
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.
A leaked private key is an emergency
Revoke it from Settings → Developers → API keys. Revocation takes effect immediately and leaves your other keys untouched, so you can mint a replacement, deploy it, and delete the old one in that order.
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_aton anything temporary: a migration script, an agency's access, a prototype. - Rotate after anyone with access to your keys leaves.