Resources

Examples

Minimal complete integrations in Next.js, PHP and Python.

Each example is a small, complete server-side integration: connect a customer, start a deposit and receive webhooks. Replace the stubbed functions (marked declare) with your own user store. Idempotency keys built from your ids must stay within 8-120 characters of letters, digits, ., _, : and -.

Environment variables used throughout:

bash
DERIPAY_API_KEY="dpk_live_…"
DERIPAY_WEBHOOK_SECRET="whsec_…"
DERIV_CLIENT_ID="your Deriv OAuth client ID"

Next.js (route handlers)

lib/deripay.ts

ts
const BASE = "https://deriapi.site/api/v2";

export class DeripayError extends Error {
  constructor(public status: number, public code: string, message: string, public requestId?: string) {
    super(message);
  }
}

export async function deripay<T>(
  path: string,
  init: { method?: string; body?: unknown; idempotencyKey?: string } = {},
): Promise<T> {
  const headers: Record<string, string> = {
    Authorization: `Bearer ${process.env.DERIPAY_API_KEY}`,
    Accept: "application/json",
  };
  if (init.body !== undefined) headers["Content-Type"] = "application/json";
  if (init.idempotencyKey) headers["Idempotency-Key"] = init.idempotencyKey;

  const res = await fetch(`${BASE}${path}`, {
    method: init.method ?? "GET",
    headers,
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
    cache: "no-store",
    signal: AbortSignal.timeout(60_000),
  });
  const json = await res.json();
  if (!json.success) {
    throw new DeripayError(res.status, json.error.code, json.error.message, json.error.request_id);
  }
  return json.data as T;
}

export interface Transaction {
  id: string;
  object: "transaction";
  type: "deposit" | "withdrawal";
  state: "pending" | "action_required" | "awaiting_code" | "processing" | "completed" | "failed";
  status: string;
  status_message: string | null;
  connection_id: string;
  country: string;
  currency: string;
  operator: string | null;
  amount: number | null;
  amount_usd: number | null;
  phone_number: string | null;
  customer_action: { step: string; redirect_url: string | null; instructions: string[] } | null;
  terminal: boolean;
  created_at: string;
  updated_at: string;
}

Connect: lib/connect.ts

Call this from your Deriv OAuth callback route as soon as you hold the access token, so the token never passes through the browser. The full callback is in Deriv OAuth.

ts
import { deripay } from "@/lib/deripay";

declare function saveConnectionId(userId: string, connectionId: string): Promise<void>;

export async function connectCustomer(userId: string, token: { access_token: string; expires_in?: number }) {
  const conn = await deripay<{ id: string; deriv_login_id: string | null; authorization_status: string }>(
    "/connections",
    {
      method: "POST",
      body: {
        client_id: process.env.DERIV_CLIENT_ID,
        deriv_access_token: token.access_token,
        token_expires_in: token.expires_in,
        external_customer_id: userId,
      },
    },
  );
  await saveConnectionId(userId, conn.id); // con_6720f1a2b3c4d5e6f7a8
  return conn;
}

Deposit: app/api/deripay/deposit/route.ts

ts
import crypto from "node:crypto";
import { deripay, DeripayError, type Transaction } from "@/lib/deripay";

declare function currentUserId(request: Request): Promise<string>;
declare function getConnectionId(userId: string): Promise<string | null>;
declare function saveTransaction(userId: string, tx: Transaction, idempotencyKey: string): Promise<void>;

export async function POST(request: Request) {
  const userId = await currentUserId(request);
  const connectionId = await getConnectionId(userId);
  if (!connectionId) return Response.json({ error: "NOT_CONNECTED" }, { status: 400 });

  const input = (await request.json()) as {
    country: string;
    amount: number;
    phone_number: string;
    operator?: string;
    // Your form generates this once per attempt and resends it on retry.
    attempt_id?: string;
  };
  if (!Number.isInteger(input.amount) || input.amount <= 0) {
    return Response.json({ error: "Whole amounts only" }, { status: 400 });
  }

  const idempotencyKey = `dep_${userId}_${input.attempt_id ?? crypto.randomUUID()}`;

  try {
    const tx = await deripay<Transaction>("/deposits", {
      method: "POST",
      idempotencyKey,
      body: {
        connection_id: connectionId,
        country: input.country,
        amount: input.amount,
        phone_number: input.phone_number,
        ...(input.operator ? { operator: input.operator } : {}),
      },
    });
    await saveTransaction(userId, tx, idempotencyKey);
    return Response.json({
      id: tx.id,
      state: tx.state,
      customer_action: tx.customer_action,
      status_message: tx.status_message,
    });
  } catch (err) {
    if (err instanceof DeripayError) {
      return Response.json({ error: err.code, message: err.message }, { status: err.status });
    }
    throw err;
  }
}

Webhook: app/api/deripay/webhook/route.ts

ts
import crypto from "node:crypto";
import type { Transaction } from "@/lib/deripay";

declare function alreadyProcessed(eventId: string): Promise<boolean>;
declare function markProcessed(eventId: string): Promise<void>;
declare function applyTransactionUpdate(tx: Transaction): Promise<void>;

function verify(rawBody: string, header: string | null, secret: string): boolean {
  if (!header) return false;
  const parts: Record<string, string> = {};
  for (const item of header.split(",")) {
    const i = item.indexOf("=");
    if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
  }
  if (!parts.t || !/^\d+$/.test(parts.t) || !parts.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

export async function POST(request: Request) {
  const raw = await request.text();
  if (!verify(raw, request.headers.get("deripay-signature"), process.env.DERIPAY_WEBHOOK_SECRET!)) {
    return new Response("bad signature", { status: 401 });
  }

  const event = JSON.parse(raw) as { id: string; type: string; created_at: string; data: any };
  if (event.type === "ping") return new Response("ok");
  if (await alreadyProcessed(event.id)) return new Response("ok");

  if (event.type.startsWith("transaction.")) {
    // Keyed on data.id, and ignores updates older than what you already hold.
    await applyTransactionUpdate(event.data as Transaction);
  }
  await markProcessed(event.id);
  return new Response("ok");
}

PHP (curl)

php
<?php
const DERIPAY_BASE = 'https://deriapi.site/api/v2';

function deripay(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array
{
    $headers = [
        'Authorization: Bearer ' . getenv('DERIPAY_API_KEY'),
        'Accept: application/json',
    ];
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
    }
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }

    $ch = curl_init(DERIPAY_BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 60,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $raw = curl_exec($ch);
    if ($raw === false) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException('Network error: ' . $error);
    }
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $json = json_decode($raw, true);
    if (!is_array($json) || empty($json['success'])) {
        $code = $json['error']['code'] ?? 'UNKNOWN';
        throw new RuntimeException("Deripay {$status} {$code}: " . ($json['error']['message'] ?? ''));
    }
    return $json['data'];
}

// Connect, after your Deriv OAuth callback has the access token.
$connection = deripay('POST', '/connections', [
    'client_id' => getenv('DERIV_CLIENT_ID'),
    'deriv_access_token' => $derivAccessToken,
    'token_expires_in' => $derivExpiresIn,
    'external_customer_id' => (string) $userId,
]);
$connectionId = $connection['id']; // con_…

// Check limits, then deposit.
$rates = deripay('GET', '/rates?country=KE');
$amount = 1000;
if (!$rates['deposits_available'] || $amount < $rates['deposit_limits']['min'] || $amount > $rates['deposit_limits']['max']) {
    throw new RuntimeException('Amount outside current limits');
}

$idempotencyKey = "dep_{$userId}_{$orderId}";
$tx = deripay('POST', '/deposits', [
    'connection_id' => $connectionId,
    'country' => 'KE',
    'amount' => $amount,
    'phone_number' => '0712345678',
], $idempotencyKey);

echo $tx['id'] . ' ' . $tx['state'] . PHP_EOL; // txn_… pending

Webhook endpoint (webhook.php):

php
<?php
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_DERIPAY_SIGNATURE'] ?? '';
$secret = getenv('DERIPAY_WEBHOOK_SECRET');

$parts = [];
foreach (explode(',', $header) as $item) {
    $kv = explode('=', trim($item), 2);
    if (count($kv) === 2) {
        $parts[$kv[0]] = $kv[1];
    }
}
$valid = isset($parts['t'], $parts['v1'])
    && ctype_digit($parts['t'])
    && abs(time() - (int) $parts['t']) <= 300
    && hash_equals(hash_hmac('sha256', $parts['t'] . '.' . $payload, $secret), $parts['v1']);

if (!$valid) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);
// Skip $event['id'] if you have seen it; otherwise update $event['data']['id'] in your records.
http_response_code(200);
echo 'ok';

Python (requests)

python
import os
import uuid

import requests

BASE = "https://deriapi.site/api/v2"


class DeripayError(Exception):
    def __init__(self, status: int, code: str, message: str):
        super().__init__(f"{status} {code}: {message}")
        self.status = status
        self.code = code


def deripay(method: str, path: str, body: dict | None = None, idempotency_key: str | None = None) -> dict:
    headers = {
        "Authorization": f"Bearer {os.environ['DERIPAY_API_KEY']}",
        "Accept": "application/json",
    }
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    res = requests.request(method, BASE + path, json=body, headers=headers, timeout=60)
    data = res.json()
    if not data.get("success"):
        err = data.get("error", {})
        raise DeripayError(res.status_code, err.get("code", "UNKNOWN"), err.get("message", ""))
    return data["data"]


# Connect, after your Deriv OAuth callback has the access token.
connection = deripay("POST", "/connections", {
    "client_id": os.environ["DERIV_CLIENT_ID"],
    "deriv_access_token": deriv_access_token,
    "token_expires_in": deriv_expires_in,
    "external_customer_id": str(user_id),
})
connection_id = connection["id"]  # con_…

# Uganda needs an operator. Read it from /markets.
markets = deripay("GET", "/markets")["markets"]
uganda = next(m for m in markets if m["country"] == "UG")
operator = uganda["operators"][0]["code"]

rates = deripay("GET", "/rates?country=UG")
amount = 50000
assert rates["deposits_available"]
assert rates["deposit_limits"]["min"] <= amount <= rates["deposit_limits"]["max"]

idempotency_key = f"dep_{user_id}_{uuid.uuid4()}"  # save this before sending
tx = deripay("POST", "/deposits", {
    "connection_id": connection_id,
    "country": "UG",
    "amount": amount,
    "phone_number": "0772123456",
    "operator": operator,
}, idempotency_key=idempotency_key)

if tx["state"] == "action_required" and tx["customer_action"]["step"] == "otp":
    otp = input("Code from your network: ")
    tx = deripay("POST", f"/deposits/{tx['id']}/otp", {"otp": otp})

print(tx["id"], tx["state"])

Webhook endpoint with Flask:

python
import hashlib
import hmac
import os
import time

from flask import Flask, request

app = Flask(__name__)


def verify(payload: bytes, header: str, secret: str) -> bool:
    parts = {}
    for item in (header or "").split(","):
        key, sep, value = item.strip().partition("=")
        if sep:
            parts[key] = value
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not t.isdigit() or not v1 or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)


@app.post("/webhooks/deripay")
def deripay_webhook():
    payload = request.get_data()
    if not verify(payload, request.headers.get("Deripay-Signature", ""), os.environ["DERIPAY_WEBHOOK_SECRET"]):
        return "bad signature", 401
    event = request.get_json()
    # Skip event["id"] if already processed; otherwise update event["data"]["id"] in your records.
    return "ok", 200