Keys & access
Every 401, 403 and 406 the API returns, and the CORS error that is not one.
Authentication failures are the easiest family to diagnose, because the API answers a different code
for each cause. The trap is that the codes are not the ones most people expect: validation is 422,
a wrong key class is 406, and there is no 409 at all.
401 Invalid or revoked API credentials
The key was not recognised as a usable credential. Four causes, and the check runs against the database on every request, so a key revoked a second ago stops working now.
| Cause | How to confirm |
|---|---|
| Typo, or a truncated paste | The value is a prefix, a dot, and 96 characters |
| The key was deleted or deactivated | It disappears from Settings, Developer, API Keys |
The key is past expires_at | The expiry is shown next to the key |
| The key belongs to the other environment | pk_test. cannot act live, pk. cannot act in sandbox |
Send it in Authorization with no Bearer prefix. The API reads the header value as the key itself,
and a Bearer in front of it makes the lookup fail like any other typo.
curl https://api.wajub.com/payments \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"403 Security Alert: You are trying to use a Private Key from a browser
An sk. key arrived with an Origin or a Referer header, which only a browser sends. Wajub
refuses the call and emails the key's owner, throttled to one alert per key per hour.
Two things follow. Move the call to your server, and treat the key as leaked: it was in a bundle someone downloaded, so create a replacement, deploy it, then delete the old one. Security has the sequence that avoids an outage.
403 IP address not allowed for this API key
The key has an allow-list and your caller is not on it. It applies to every key type, including public ones, and accepts exact IPv4 or IPv6 addresses as well as CIDR ranges in either family. An empty list means no restriction.
The usual cause is a deploy that changed your egress address, or a job running from a machine nobody added. Check the caller's real IP against the list on the key.
403 This API key does not have permission to read payment
A restricted key without the scope the route needs. Scopes are {resource}.{read|write}, and the
verb decides which half applies: GET, HEAD and OPTIONS need read, everything else needs
write.
There is a second shape of this error worth knowing.
That one is not about a missing scope. A restricted key is an allow-list, not a filter: if a route
does not map to one of the scoped resources, no restricted key reaches it, whatever you granted.
Four paths are exempt because they carry nothing sensitive: /channels, /countries, /currencies
and the API root.
Two resources are read only, so a restricted key can never write them: balance and account. In
practice that means a restricted key cannot create a Sync connection. Use a private key on your
server for that.
406 Private Key Required
The route needs a private key and you sent a public one. It covers /balance, /transfers,
/refunds, /disputes and /beneficiaries.
/payments and /accounts are not in that list, which surprises people in both directions: a public
key can create a payment, and a public key can create a Sync connection. That a call is accepted
from a public key does not mean it belongs in a browser.
"I get a CORS error"
Almost certainly not. Every response the API returns, errors included, carries
Access-Control-Allow-Origin: *, and preflight OPTIONS answers 204 with the allowed methods and
headers. Wajub does not produce CORS failures.
What you are seeing is one of these, reported by the browser as a failed request.
| What really happened | Tell |
|---|---|
The 403 browser block above | A private key in client code |
| The request never left | A bad URL, an offline tab, a blocked extension |
| A custom header you added | Only the listed headers are allowed on preflight |
That last row is the one worth checking in the network tab. Preflight allows Content-Type,
Authorization, X-Requested-With, Accept, Origin, Idempotency-Key and X-Link-View-Token.
A header outside that set fails preflight, and the browser reports a CORS error for a request that
was never rejected on its merits.
404 on an id I am certain exists
Sandbox and live are different databases, chosen by your key's prefix before a single query runs. An
id from one is genuinely absent from the other, so the answer is 404 rather than a permission
error. Sandbox ids carry test_ after the prefix, which tells you at a glance which one you are
holding.
The same applies across teams. Ids do not resolve outside the team that owns them, and the response
is 404 on purpose: a 403 would confirm the object exists.
422 with errors.idempotency_key
You reused an Idempotency-Key with a different payload. Either you changed an amount without
changing the key, or you reused a key you meant to rotate. It is not transient, and retrying
produces the same answer.
The key format is also validated before anything else: A-Z, a-z, 0-9 and . _ : -, from 1 to
128 characters. An order number written #4172, or a UUID with braces, is refused on the format
alone.