Skip to content

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:

MessageCauseHow to confirm
Invalid API credentialsTypo, or a truncated pasteThe value matches the key shown in the Dashboard: a prefix, a dot, and the whole secret
Invalid API credentialsA Bearer prefix in front of the keySee below
Invalid API credentialsThe key was deleted or deactivatedA deleted key disappears from Settings, Developer, API Keys; a deactivated one stays there, marked Disabled
Invalid API credentialsThe key belongs to the other environmentpk_test. cannot act live, pk. cannot act in sandbox
Invalid or revoked API credentialsThe key is past expires_at, or was revoked in the last momentsThe 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.

The header the API expects
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.

A restricted key on a route that maps to no resource
{
"code": 403,
"status": "Forbidden",
"message": "This API key cannot access this resource. Restricted keys can only access specific resources."
}

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 happenedTell
The 403 browser block aboveA private key in client code
The request never leftA bad URL, an offline tab, a blocked extension
A custom header you addedOnly 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.

What did you think of this content?