Keys & access
Every refusal of a key, an account or a version (400, 401, 403, 406, 429), 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 the only 409 among authentication problems is wajub logout on
a key that wajub login did not create.
401 Invalid API credentials
The key was not recognised as a usable credential. Two messages come back, and the message tells you which cause to look for:
| Message | Cause | How to confirm |
|---|---|---|
Invalid API credentials | Typo, or a truncated paste | The value matches the key shown in the Dashboard: a prefix, a dot, and the whole secret |
Invalid API credentials | A Bearer prefix in front of the key | See below |
Invalid API credentials | The key was deleted or deactivated | A deleted key disappears from Settings, Developer, API Keys; a deactivated one stays there, marked Disabled |
Invalid API credentials | The key belongs to the other environment | pk_test. cannot act live, pk. cannot act in sandbox |
Invalid or revoked API credentials | The key is past expires_at, or was revoked in the last moments | The expiry is shown next to the key |
The key is checked on every request, so a key revoked a second ago stops working now.
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.xxxxxxxxxxxxxxxx…"403 Security Alert: You are trying to use a Private Key from a browser
Wajub detected an sk. key used from a browser. It refuses the call and emails the key's owner.
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, /beneficiaries, /webhooks, /events, /identity, /accounts, /tax,
/shield, /listen/sessions, a customer's tax_ids, POST /payments/{id}/activate and
PATCH /payments/{id}, so a Sync connection is created from your server only. The full table is in
Authentication.
/payments is not in that list: a public key can create a payment. That a call is accepted from a
public key does not mean it belongs in a browser.
403 API access is currently restricted for this account. Contact support.
Wajub has restricted the account, and the restriction closes the whole API, whatever key you send.
The body carries error_code: merchant_restricted and the restriction's type in restriction. A
narrower restriction refuses only some calls, with its own error_code: see
Restriction errors. Contact support.
403 This feature is only available in live mode.
Links, invoices and a few other live-only routes refuse a sandbox key. Call them with a live key once the account is verified.
403 … is not included in your plan
Sync (/accounts, any call with X-Sync) and Tax (/tax) need the product in your plan or an
active add-on. The body carries error_code: feature_not_in_plan.
403 on a call with X-Sync
You are not authorized to access this Sync account. and Sync account not found. mean the
connection is not yours or does not exist. Sync account is not active. names its status.
This Sync account does not have the '…' permission. means the merchant did not grant that
capability. Payments and withdrawals are suspended for this Sync account. refuses only the calls
that move money while the merchant's payments are suspended; reads still work.
400 Unsupported API version
The X-Wajub-Version header names a version the API does not serve. The message lists the ones it
does. This is checked before the key, so it can come back on a call whose key is also wrong.
429 Too many requests
A rate limit, also checked before the key. type says which counter tripped and retry_after how
long to wait: see Rate limits.
"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, X-Request-Id, Idempotency-Key, X-Idempotency-Key,
X-Sync, Accept, Origin and X-Wajub-Version.
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 share no data, and your key's prefix decides which one you reach. 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.