Guides

Deriv OAuth

Register a Deriv OAuth app and connect a customer with Authorization Code and PKCE, or with a personal access token.

Deripay never asks your customers for their Deriv password and never issues its own customer tokens. Your customer signs in to Deriv through your Deriv OAuth app. Your backend gets a short-lived Deriv access token and hands it to Deripay once, in POST /connections. Deripay returns a connection id that you use from then on.

Customers who prefer to paste a Deriv personal access token (PAT) can connect that way too. See Personal access tokens.

What belongs to whom:

Step Owned by
OAuth app, client ID, redirect URI, sign-in page, token endpoint Deriv
state, PKCE code_verifier, callback handler, token exchange Your backend
POST /api/v2/connections and the con_… id Deripay

1. Register the app on Deriv

Create an OAuth app on developers.deriv.com.

  • Scopes: trade account_manage payment. All three are required. trade lets Deripay list the customer's accounts, account_manage resolves the customer's payment agent transfer name, and payment allows balance reads and withdrawals.
  • Redirect URI: an https URL on your site, for example https://yoursite.com/api/deriv/callback.

Deriv gives you a client ID made of letters and digits. Deripay needs this new OAuth client ID. Old numeric Deriv App IDs are rejected.

If you add a scope later, customers who connected before must connect again. Existing tokens do not gain new permissions.

2. Add the app to Deripay

In the Deripay dashboard, open Deriv apps and add:

  • Client ID: the Deriv OAuth client ID.
  • Redirect URI: the same callback URL you registered on Deriv. It must use https.
  • Site domain: shown against this app's transactions.

A client ID can belong to one Deripay account only. POST /connections with a client ID that is not registered and active on your account fails with 400 DERIV_APP_NOT_REGISTERED.

3. Run the flow on your backend

Deriv uses OAuth 2.0 Authorization Code with PKCE:

  • Authorization endpoint: https://auth.deriv.com/oauth2/auth
  • Token endpoint: https://auth.deriv.com/oauth2/token

For the exact parameters Deriv accepts, and whether your app also has a client secret, see developers.deriv.com/docs/intro/oauth. The example below uses the standard OAuth 2.0 parameter names.

Rules:

  • Create a fresh state and code_verifier for every sign-in attempt.
  • Use code_challenge_method=S256.
  • Check state before you exchange the code.
  • redirect_uri must match the registered URI exactly.
  • Keep the access token in memory only long enough to call POST /connections. Do not store it, log it or send it to the browser.

Node example (Next.js route handlers)

app/api/deriv/login/route.ts starts the flow:

ts
import crypto from "node:crypto";

const CLIENT_ID = process.env.DERIV_CLIENT_ID!;
const REDIRECT_URI = process.env.DERIV_REDIRECT_URI!; // https://yoursite.com/api/deriv/callback

const b64url = (buf: Buffer) => buf.toString("base64url");

export async function GET() {
  const state = b64url(crypto.randomBytes(24));
  const verifier = b64url(crypto.randomBytes(48));
  const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());

  const url = new URL("https://auth.deriv.com/oauth2/auth");
  url.searchParams.set("response_type", "code");
  url.searchParams.set("client_id", CLIENT_ID);
  url.searchParams.set("redirect_uri", REDIRECT_URI);
  url.searchParams.set("scope", "trade account_manage payment");
  url.searchParams.set("state", state);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set("code_challenge_method", "S256");

  const cookie = (name: string, value: string) =>
    `${name}=${value}; Path=/api/deriv; HttpOnly; Secure; SameSite=Lax; Max-Age=600`;

  const headers = new Headers({ Location: url.toString() });
  headers.append("Set-Cookie", cookie("deriv_state", state));
  headers.append("Set-Cookie", cookie("deriv_verifier", verifier));
  return new Response(null, { status: 302, headers });
}

app/api/deriv/callback/route.ts finishes it and creates the connection:

ts
const CLIENT_ID = process.env.DERIV_CLIENT_ID!;
const REDIRECT_URI = process.env.DERIV_REDIRECT_URI!;
const DERIPAY = "https://deriapi.site/api/v2";

function readCookie(request: Request, name: string): string | null {
  const raw = request.headers.get("cookie") || "";
  for (const part of raw.split(";")) {
    const [k, ...v] = part.trim().split("=");
    if (k === name) return v.join("=");
  }
  return null;
}

export async function GET(request: Request) {
  const url = new URL(request.url);
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");
  const expectedState = readCookie(request, "deriv_state");
  const verifier = readCookie(request, "deriv_verifier");

  if (!code || !state || !expectedState || state !== expectedState || !verifier) {
    return new Response("Sign-in expired. Please try again.", { status: 400 });
  }

  // 1. Exchange the code with Deriv.
  const tokenRes = await fetch("https://auth.deriv.com/oauth2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: verifier,
    }),
  });
  if (!tokenRes.ok) return new Response("Deriv sign-in failed.", { status: 502 });
  const token = (await tokenRes.json()) as { access_token: string; expires_in?: number };

  // 2. Hand the token to Deripay.
  const userId = await currentUserId(request); // your own session lookup
  const res = await fetch(`${DERIPAY}/connections`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.DERIPAY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      client_id: CLIENT_ID,
      deriv_access_token: token.access_token,
      token_expires_in: token.expires_in,
      external_customer_id: userId,
    }),
  });
  const body = await res.json();
  if (!body.success) {
    return new Response(`Could not connect: ${body.error.code}`, { status: 400 });
  }

  // 3. Store the connection id against your user.
  await saveConnectionId(userId, body.data.id); // e.g. con_6720f1a2b3c4d5e6f7a8

  const headers = new Headers({ Location: "/cashier" });
  headers.append("Set-Cookie", "deriv_state=; Path=/api/deriv; Max-Age=0");
  headers.append("Set-Cookie", "deriv_verifier=; Path=/api/deriv; Max-Age=0");
  return new Response(null, { status: 302, headers });
}

// Replace these with your own user store.
declare function currentUserId(request: Request): Promise<string>;
declare function saveConnectionId(userId: string, connectionId: string): Promise<void>;

4. Create the connection

http
POST /api/v2/connections
Authorization: Bearer dpk_live_…
Content-Type: application/json
json
{
  "client_id": "YOUR_DERIV_CLIENT_ID",
  "deriv_access_token": "DERIV_ACCESS_TOKEN",
  "token_expires_in": 3600,
  "external_customer_id": "user_1842",
  "preferred_account_id": "CR1234567"
}
Field Required Notes
client_id yes Your Deriv OAuth client ID, registered under Deriv apps. 6 to 64 characters.
deriv_access_token yes The access token from the token exchange, or the customer's personal access token. It must carry trade account_manage payment.
token_expires_in no expires_in from Deriv's token response, in seconds. Recorded for reference only. Leave it out for a personal access token.
external_customer_id no Your own id for this customer, up to 128 characters. Lets you find the connection later.
preferred_account_id no The Deriv login id to use by default, for example CR1234567. Must be a real USD account.

The customer needs at least one real USD Deriv account. Demo and non-USD accounts cannot move money. The response is a connection.

Personal access tokens

A customer can also connect with a Deriv personal access token (PAT) instead of signing in through OAuth. This suits sites that offer a "log in with token" option.

  1. The customer creates a PAT in their Deriv account with the scopes trade account_manage payment. A PAT without payment cannot move money.
  2. They paste it into your site, and your backend sends it to POST /connections as deriv_access_token, together with your registered client_id. Leave out token_expires_in.
  3. From then on the connection works exactly like one made through OAuth: deposits, withdrawals and balance reads all use the con_… id.

Handle a PAT like a password:

  • Send it straight from your backend to Deripay. Do not log it, keep it in browser storage or put it in a URL.
  • Do not store it after POST /connections returns.
  • Your Deriv app still has to be registered under Deriv apps, or the call fails with DERIV_APP_NOT_REGISTERED.

When a PAT stops working (the customer revoked it, or it lacks a scope), the reconnect errors below apply. Ask the customer for a new PAT with the right scopes and call POST /connections again.

Reconnecting

Deriv access tokens are short-lived and Deriv does not return a refresh token. Deripay keeps using the token until Deriv itself rejects it. Then:

  • calls that need the customer's Deriv authorization fail with DERIV_RECONNECT_REQUIRED (or a prefixed form such as DEPOSIT_DERIV_RECONNECT_REQUIRED), and
  • GET /connections/{id}?refresh=true shows authorization_status: "reconnect_required".

Run the OAuth flow again (or ask for a new personal access token) and call POST /connections with the new token. Keep the customer signed in to your own site while they reconnect. Do not decide to reconnect by comparing token_expires_in with the clock.

Also reconnect on DERIV_AUTH_FAILED, DERIV_ACCOUNT_NOT_ELIGIBLE and *_SESSION_APP_MISMATCH. Do not reconnect on DERIV_REFRESH_UNAVAILABLE. That one is temporary: retry with backoff. See Errors.