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:
DERIPAY_API_KEY="dpk_live_…"
DERIPAY_WEBHOOK_SECRET="whsec_…"
DERIV_CLIENT_ID="your Deriv OAuth client ID"Next.js (route handlers)
lib/deripay.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.
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
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
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
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_… pendingWebhook endpoint (webhook.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)
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:
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