Common errors, decoded.
Real errors developers hit while integrating Deripay, and exactly how to fix each one.
DERIV_AUTH_FAILED — Invalid Deriv API token
Upstream rejected the token you supplied in userToken. Note that this error message is sometimes a relay of one of three different Deriv-side errors. Check them in this order.
1. Wrong or unregistered app_id
The Deriv API will return InvalidAppID if the app_idbound to your Deripay API key isn't a real app on Deriv. Test it directly with a 10-line script:
# In your project (or any Node script), against Deriv's WS endpoint:
node -e "
const ws = new (require('ws'))('wss://ws.derivws.com/websockets/v3?app_id=YOUR_APP_ID');
ws.on('open', () => ws.send(JSON.stringify({ authorize: 'YOUR_TOKEN' })));
ws.on('message', d => { console.log(JSON.parse(d.toString())); ws.close(); });
"If you see { error: { code: "InvalidAppID" } } the app_id is wrong. Register the app at the Deriv DevHub, take the numeric id Deriv assigns, then in your portal go to API Keys → Edit → Deriv app ID and update it.
2. OAuth token from a different app
OAuth tokens are bound to the app_id that minted them — if you mint a token under app_id A and then authorize it against app_id B, Deriv rejects it with "Token is not valid for current app ID". Make sure the app_id on your Deripay API key matches the one that issued the token.
Manual tokens from app.deriv.com/account/api-token are different — they authorize against any app_id. The only requirement is ticking the payments scope when you create them; without it, both deposits and withdrawals fail with insufficient scope (see below).
For OAuth, mint a fresh token against your real app:
https://oauth.deriv.com/oauth2/authorize
?app_id=<your-real-deriv-app-id>
&l=EN
&scope=read,trading_information,payments
&redirect_uri=<your-registered-redirect>After login Deriv redirects to your URL with ?acct1=CR1234567&token1=a1-xxxxxx&... — use token1 as userToken.
3. Token expired or revoked
OAuth tokens expire after ~7 days of inactivity. Manual revocation (the user clicked "revoke" in their Deriv settings) is immediate. Re-mint via OAuth.
Deposits or withdrawals fail with insufficient scope
Both directions need the payments scope on the userToken. Deripay routes deposits through Deriv's paymentagent_transfer and withdrawals through paymentagent_withdraw — and both are payment-agent APIs that Deriv gates behind the paymentsscope. It's not in the default scope set, so a token minted without it will fail on either flow.
Test what scopes your token has:
// authorize response includes a scopes array:
{
authorize: {
loginid: "CR3676771",
scopes: [ "read", "trading_information", "payments" ]
^^^^^^^^^^ ← required for both deposits & withdrawals
}
}For OAuth tokens two things must line up:
- Your Deriv app (in the DevHub) must be registered with
paymentsin its allowed-scopes list — edit the app in DevHub and tick the scope. Without this, Deriv silently dropspaymentsfrom the OAuth response even if your authorize URL asked for it. The token comes back looking valid, but neither deposits nor withdrawals will work. - Your OAuth URL must request it:
&scope=read,trading_information,payments.
For manual tokens from app.deriv.com/account/api-tokenthere's only one step: tick the Paymentscheckbox when you create the token. There's no DevHub-side gate — manual tokens can carry paymentsregardless of which app you authorize against, and once it's set both deposits and withdrawals work.
Bottom line: payments is non-negotiable on anytoken used with Deripay — OAuth or manual, deposit or withdrawal. If a token doesn't have it, every payment-agent call on Deriv's side will fail.
ORIGIN_NOT_ALLOWED — Origin doesn't match key's domain
Only fires when the API key has strict mode (requireOrigin: true) enabled. The browser sent an Originheader that doesn't match the domain bound to the key.
Server-to-server calls without an Origin header skip the check entirely (the API key + HMAC signature are the primary credentials). To fix:
- Make sure the browser's origin matches the key's bound domain exactly. Strict mode does single-domain matching — there's no allowlist of multiple verified domains anymore.
- If you need a different domain, rotate the key (preserves domain) — no, that won't help here. Revoke + create a new key with the right domain.
- If you don't actually need browser-allowlisting, edit the key in /api-keys and uncheck Require verified Origin.
STALE_TIMESTAMP — X-Timestamp is too old or in the future
The signed request must arrive within ±300 seconds of our server clock. If your server's clock drifts, signatures will fail.
- Generate the timestamp inside the same function that signs the request — never reuse a stored timestamp from earlier.
- On servers, verify NTP sync is healthy (
timedatectl statuson Linux). - Don't generate timestamps in the browser and pass them to the server — sign on the server only, where the clock is reliable.
BAD_SIGNATURE — Signature verification failed
The HMAC didn't match. Common causes:
- Body was modified between signing and sending — make sure you
JSON.stringifythe exact same object you send. - Path mismatch — the
pathin the signing payload must match exactly:/api/v1/deposit, nothttps://deripay.site/api/v1/deposit. - Newline character — the signing payload uses literal
\n, not\r\n. - Body hash on a wrong representation — for empty bodies use
sha256(""), notsha256("").
Reference signing snippet from the quickstart — copy it verbatim if in doubt.
RATE_LIMITED — Rate limit exceeded
Per-API-key per-route limits:
/rates— 120 / min/deposit— 30 / min/withdrawals/initiate— 30 / min/withdrawals/confirm— 30 / min/transactions/:id— 240 / min
Polling at 5-second intervals stays well under the transactions limit even if you have many transactions in flight. The response includes a retryAfter field (seconds) and a Retry-After header — back off and retry.
Webhook not firing
Common reasons a transaction completes but no webhook arrives:
- The webhook in your portal is
disabled— check the status pill. After 10 consecutive failures we auto-disable. - The event type is not subscribed — make sure
transaction.completed/transaction.failed/transaction.cancelledare toggled on. - Your URL responded non-2xx within 10 seconds — check your server's access logs and the delivery history in the portal.
- Your URL is on a private IP or non-HTTPS in production — these are rejected at registration time.
- You triggered the transaction outside our system — only transactions created through
/api/v1/depositor/api/v1/withdrawals/initiatedispatch webhooks.
500 errors with no detail in your server logs
Old versions of the gateway returned 500 without logging the underlying cause. As of v1.x the server now logs every /api/v1/* error to the dev-server console with:
- The route path
- HTTP status + error code
- Error message
- Upstream response body (when applicable)
- The first 6 frames of the stack
Check the terminal where you ran npm run dev — the message will be on the line right above the POST /api/v1/… 500 in …ms log.
Diagnostic scripts
Two scripts shipped with the gateway codebase that help isolate problems:
scripts/test-deriv-token.ts
Authorize a Deriv token directly against Deriv's WS — bypasses Deripay entirely so you can tell whether the problem is the token, the app_id, or our gateway.
npx tsx scripts/test-deriv-token.ts <token> <app_id>
# Outputs scopes, account list, landing company, and any error.scripts/inspect-payment-agent-txns-schema.ts
Dump the full Appwrite collection schema with required/optional flags. Useful when an Appwrite Missing required attribute "X" error appears.
npx tsx scripts/inspect-payment-agent-txns-schema.tsStill stuck?
Drop the following into a support ticket and we'll help fast:
- The request ID from the response (always returned on errors).
- The exact HTTP status + code you received.
- The app_id bound to your API key (visible in the portal — never paste your secret).
- Output of
test-deriv-token.tswith the same token your server is using.