Code examples
One file per language: request signing + sending + webhook verification, standard library only (Python uses requests). Copy them into your project as-is.
Every implementation is verified against the test vectors on the Authentication and Webhooks pages.
Client
go
// DTPG Wallet merchant API — Go example: request signing + webhook verification (standard library only).
package dtpgwallet
import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
type Client struct {
BaseURL string // https://api.dtpg.net
APIKey string
Secret string
HTTP *http.Client
}
// APIError is the unified error body. Branch on Reason; Message is for developers only.
type APIError struct {
Code int `json:"code"`
Reason string `json:"reason"`
Message string `json:"message"`
Metadata map[string]string `json:"metadata"`
}
func (e *APIError) Error() string { return fmt.Sprintf("%d %s: %s", e.Code, e.Reason, e.Message) }
// CanonicalQuery sorts by key (then by value for repeated keys), RFC 3986-encodes (space = %20) and joins with &.
func CanonicalQuery(q url.Values) string {
keys := make([]string, 0, len(q))
for k := range q {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
vs := append([]string(nil), q[k]...)
sort.Strings(vs)
for _, v := range vs {
parts = append(parts, rfc3986(k)+"="+rfc3986(v))
}
}
return strings.Join(parts, "&")
}
func rfc3986(s string) string { return strings.ReplaceAll(url.QueryEscape(s), "+", "%20") }
func hmacHex(secret, msg string) string {
m := hmac.New(sha256.New, []byte(secret))
m.Write([]byte(msg))
return hex.EncodeToString(m.Sum(nil))
}
// Sign returns X-Signature = hex(HMAC-SHA256(secret, METHOD\nPATH\nQUERY\nTIMESTAMP\nNONCE\nhex(sha256(BODY)))).
func Sign(secret, method, path, canonicalQuery, timestamp, nonce string, body []byte) string {
h := sha256.Sum256(body)
return hmacHex(secret, strings.Join([]string{method, path, canonicalQuery, timestamp, nonce, hex.EncodeToString(h[:])}, "\n"))
}
// Do sends a signed request. body == nil means no request body; out receives the response entity.
func (c *Client) Do(ctx context.Context, method, path string, query url.Values, body, out any) error {
var raw []byte
if body != nil {
var err error
if raw, err = json.Marshal(body); err != nil {
return err
}
}
// Send the canonical query string itself, so what is signed is exactly what is sent.
q := CanonicalQuery(query)
u := c.BaseURL + path
if q != "" {
u += "?" + q
}
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
nb := make([]byte, 16)
if _, err := rand.Read(nb); err != nil {
return err
}
nonce := hex.EncodeToString(nb)
req, err := http.NewRequestWithContext(ctx, method, u, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", c.APIKey)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", Sign(c.Secret, method, req.URL.EscapedPath(), q, ts, nonce, raw))
hc := c.HTTP
if hc == nil {
hc = &http.Client{Timeout: 15 * time.Second}
}
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
e := &APIError{Code: resp.StatusCode}
_ = json.Unmarshal(data, e)
return e
}
if out == nil {
return nil
}
return json.Unmarshal(data, out)
}
// VerifyWebhook checks the webhook signature and returns the body.
// X-Signature = hex(HMAC-SHA256(webhook secret, X-Timestamp + "\n" + body)); timestamps older than 5 minutes are rejected as replays.
func VerifyWebhook(secret string, r *http.Request) ([]byte, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
ts := r.Header.Get("X-Timestamp")
ms, err := strconv.ParseInt(ts, 10, 64)
if err != nil || time.Since(time.UnixMilli(ms)).Abs() > 5*time.Minute {
return nil, fmt.Errorf("webhook timestamp invalid")
}
want := hmacHex(secret, ts+"\n"+string(body))
if !hmac.Equal([]byte(want), []byte(strings.ToLower(r.Header.Get("X-Signature")))) {
return nil, fmt.Errorf("webhook signature invalid")
}
return body, nil
}java
// DTPG Wallet merchant API — Java 17+ example: request signing + webhook verification (JDK only).
// Request/response bodies are JSON strings; plug in Jackson/Gson as you like.
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class DtpgWallet {
private static final HexFormat HEX = HexFormat.of();
private static final SecureRandom RANDOM = new SecureRandom();
private final String baseUrl;
private final String apiKey;
private final String secret;
private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
public DtpgWallet(String baseUrl, String apiKey, String secret) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.secret = secret;
}
/** RFC 3986: URLEncoder uses "+" for space and leaves "*" alone; the server expects %20 and %2A, and "~" unescaped. */
static String rfc3986(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8).replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
}
/** Sort by key (then by value), RFC 3986-encode, join with &. */
public static String canonicalQuery(Map<String, List<String>> params) {
List<String> parts = new ArrayList<>();
new TreeMap<>(params).forEach((k, vs) -> vs.stream().sorted().forEach(v -> parts.add(rfc3986(k) + "=" + rfc3986(v))));
return String.join("&", parts);
}
static String sha256Hex(byte[] data) {
try {
return HEX.formatHex(MessageDigest.getInstance("SHA-256").digest(data));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
static String hmacHex(String secret, byte[] msg) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HEX.formatHex(mac.doFinal(msg));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/** X-Signature = hex(HMAC-SHA256(secret, METHOD\nPATH\nQUERY\nTIMESTAMP\nNONCE\nhex(sha256(BODY)))) */
public static String sign(String secret, String method, String path, String query, String timestamp, String nonce, byte[] body) {
String toSign = String.join("\n", method, path, query, timestamp, nonce, sha256Hex(body));
return hmacHex(secret, toSign.getBytes(StandardCharsets.UTF_8));
}
/** Sends a signed request; jsonBody may be null. Returns the response body; throws DtpgWalletException on non-200. */
public String request(String method, String path, Map<String, List<String>> query, String jsonBody) throws Exception {
byte[] raw = jsonBody == null ? new byte[0] : jsonBody.getBytes(StandardCharsets.UTF_8); // sign exactly what you send
String q = query == null ? "" : canonicalQuery(query);
String ts = String.valueOf(System.currentTimeMillis());
byte[] nb = new byte[16];
RANDOM.nextBytes(nb);
String nonce = HEX.formatHex(nb);
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + path + (q.isEmpty() ? "" : "?" + q)))
.timeout(Duration.ofSeconds(15))
.header("Content-Type", "application/json")
.header("X-Api-Key", apiKey)
.header("X-Timestamp", ts)
.header("X-Nonce", nonce)
.header("X-Signature", sign(secret, method, path, q, ts, nonce, raw))
.method(method, raw.length == 0 ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofByteArray(raw))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) {
throw new DtpgWalletException(resp.statusCode(), resp.body()); // body: {code, reason, message, metadata}
}
return resp.body();
}
/** rawBody must be the untouched request body. Rejects timestamps older than 5 minutes. */
public static boolean verifyWebhook(String secret, String timestamp, String signature, byte[] rawBody) {
try {
if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 5 * 60 * 1000) return false;
} catch (NumberFormatException e) {
return false;
}
byte[] msg = (timestamp + "\n").getBytes(StandardCharsets.UTF_8);
byte[] all = new byte[msg.length + rawBody.length];
System.arraycopy(msg, 0, all, 0, msg.length);
System.arraycopy(rawBody, 0, all, msg.length, rawBody.length);
byte[] want = hmacHex(secret, all).getBytes(StandardCharsets.UTF_8);
byte[] got = (signature == null ? "" : signature.toLowerCase()).getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(want, got);
}
public static final class DtpgWalletException extends Exception {
public final int status;
public final String body;
DtpgWalletException(int status, String body) {
super(status + " " + body);
this.status = status;
this.body = body;
}
}
}py
"""DTPG Wallet merchant API — Python 3.8+ example: request signing + webhook verification (requires `requests`)."""
import hashlib
import hmac
import json
import secrets
import time
from urllib.parse import quote
import requests
def _rfc3986(s: str) -> str:
return quote(s, safe="-_.~")
def canonical_query(params: dict) -> str:
"""Sort by key (then by value), RFC 3986-encode (space = %20), join with &. Values may be lists."""
pairs = []
for k, v in (params or {}).items():
if v is None:
continue
for item in v if isinstance(v, (list, tuple)) else [v]:
pairs.append((str(k), str(item)))
pairs.sort()
return "&".join(f"{_rfc3986(k)}={_rfc3986(v)}" for k, v in pairs)
def _hmac_hex(secret: str, msg: bytes) -> str:
return hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
def sign(secret: str, method: str, path: str, query: str, timestamp: str, nonce: str, body: bytes = b"") -> str:
"""X-Signature = hex(HMAC-SHA256(secret, METHOD\\nPATH\\nQUERY\\nTIMESTAMP\\nNONCE\\nhex(sha256(BODY))))"""
to_sign = "\n".join([method, path, query, timestamp, nonce, hashlib.sha256(body).hexdigest()])
return _hmac_hex(secret, to_sign.encode())
class DtpgWalletError(Exception):
def __init__(self, status: int, body: dict):
super().__init__(f"{status} {body.get('reason')}: {body.get('message')}")
self.status = status
self.reason = body.get("reason")
self.metadata = body.get("metadata") or {}
class DtpgWallet:
def __init__(self, api_key: str, secret: str, base_url: str = "https://api.dtpg.net"):
self.api_key, self.secret, self.base_url = api_key, secret, base_url
self.session = requests.Session()
def request(self, method: str, path: str, query: dict = None, body: dict = None) -> dict:
raw = b"" if body is None else json.dumps(body, separators=(",", ":")).encode() # sign exactly what you send
q = canonical_query(query)
ts = str(int(time.time() * 1000))
nonce = secrets.token_hex(16)
headers = {
"Content-Type": "application/json",
"X-Api-Key": self.api_key,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": sign(self.secret, method, path, q, ts, nonce, raw),
}
url = self.base_url + path + (f"?{q}" if q else "")
resp = self.session.request(method, url, data=raw or None, headers=headers, timeout=15)
data = resp.json() if resp.content else {}
if resp.status_code != 200:
raise DtpgWalletError(resp.status_code, data)
return data
def verify_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
"""raw_body must be the untouched request body. Rejects timestamps older than 5 minutes."""
try:
if abs(time.time() * 1000 - int(timestamp)) > 5 * 60 * 1000:
return False
except (TypeError, ValueError):
return False
want = _hmac_hex(secret, timestamp.encode() + b"\n" + raw_body)
return hmac.compare_digest(want, (signature or "").lower())js
// DTPG Wallet merchant API — Node.js (18+) example: request signing + webhook verification (no dependencies).
import crypto from 'node:crypto'
// RFC 3986: encodeURIComponent leaves !'()* unescaped, the server encodes them.
const rfc3986 = (s) =>
encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
// params: { key: value | value[] }. Sort by key (then by value), RFC 3986-encode, join with &.
export function canonicalQuery(params = {}) {
const pairs = []
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null) continue
for (const item of Array.isArray(v) ? v : [v]) pairs.push([k, String(item)])
}
pairs.sort(([ak, av], [bk, bv]) => cmp(ak, bk) || cmp(av, bv))
return pairs.map(([k, v]) => `${rfc3986(k)}=${rfc3986(v)}`).join('&')
}
const sha256Hex = (data) => crypto.createHash('sha256').update(data).digest('hex')
const hmacHex = (secret, msg) => crypto.createHmac('sha256', secret).update(msg).digest('hex')
// X-Signature = hex(HMAC-SHA256(secret, METHOD\nPATH\nQUERY\nTIMESTAMP\nNONCE\nhex(sha256(BODY))))
export function sign(secret, method, path, query, timestamp, nonce, body = '') {
return hmacHex(secret, [method, path, query, timestamp, nonce, sha256Hex(body)].join('\n'))
}
export class DtpgWalletError extends Error {
constructor(status, body) {
super(`${status} ${body.reason ?? ''}: ${body.message ?? ''}`)
Object.assign(this, { status, reason: body.reason, metadata: body.metadata ?? {} })
}
}
export class DtpgWallet {
constructor({ baseURL = 'https://api.dtpg.net', apiKey, secret }) {
Object.assign(this, { baseURL, apiKey, secret })
}
async request(method, path, { query, body } = {}) {
const raw = body === undefined ? '' : JSON.stringify(body) // sign exactly the bytes you send
const q = canonicalQuery(query)
const ts = String(Date.now())
const nonce = crypto.randomBytes(16).toString('hex')
const res = await fetch(this.baseURL + path + (q ? `?${q}` : ''), {
method,
body: raw || undefined,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': this.apiKey,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': sign(this.secret, method, path, q, ts, nonce, raw),
},
})
const data = await res.json().catch(() => ({}))
if (!res.ok) throw new DtpgWalletError(res.status, data)
return data
}
}
// rawBody must be the untouched request body (Buffer/string), not re-serialized JSON.
export function verifyWebhook(secret, headers, rawBody) {
const ts = headers['x-timestamp']
if (!ts || Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false
const want = Buffer.from(hmacHex(secret, Buffer.concat([Buffer.from(ts + '\n'), Buffer.from(rawBody)])))
const got = Buffer.from(String(headers['x-signature'] ?? '').toLowerCase())
return want.length === got.length && crypto.timingSafeEqual(want, got)
}php
<?php
// DTPG Wallet merchant API — PHP 8+ example: request signing + webhook verification (ext-curl).
final class DtpgWalletException extends RuntimeException
{
public function __construct(public readonly int $status, public readonly array $body)
{
parent::__construct($status . ' ' . ($body['reason'] ?? '') . ': ' . ($body['message'] ?? ''));
}
public function reason(): ?string { return $this->body['reason'] ?? null; }
public function metadata(): array { return $this->body['metadata'] ?? []; }
}
final class DtpgWallet
{
public function __construct(
private string $apiKey,
private string $secret,
private string $baseUrl = 'https://api.dtpg.net',
) {}
/** Sort by key (then by value), RFC 3986-encode (rawurlencode), join with &. Values may be arrays. */
public static function canonicalQuery(array $params): string
{
$pairs = [];
foreach ($params as $k => $v) {
if ($v === null) continue;
foreach ((array) $v as $item) $pairs[] = [(string) $k, (string) $item];
}
usort($pairs, fn ($a, $b) => strcmp($a[0], $b[0]) ?: strcmp($a[1], $b[1]));
return implode('&', array_map(fn ($p) => rawurlencode($p[0]) . '=' . rawurlencode($p[1]), $pairs));
}
/** X-Signature = hex(HMAC-SHA256(secret, METHOD\nPATH\nQUERY\nTIMESTAMP\nNONCE\nhex(sha256(BODY)))) */
public static function sign(string $secret, string $method, string $path, string $query, string $ts, string $nonce, string $body = ''): string
{
return hash_hmac('sha256', implode("\n", [$method, $path, $query, $ts, $nonce, hash('sha256', $body)]), $secret);
}
public function request(string $method, string $path, array $query = [], ?array $body = null): array
{
$raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // sign exactly what you send
$q = self::canonicalQuery($query);
$ts = (string) (int) (microtime(true) * 1000);
$nonce = bin2hex(random_bytes(16));
$ch = curl_init($this->baseUrl . $path . ($q !== '' ? "?$q" : ''));
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Api-Key: ' . $this->apiKey,
'X-Timestamp: ' . $ts,
'X-Nonce: ' . $nonce,
'X-Signature: ' . self::sign($this->secret, $method, $path, $q, $ts, $nonce, $raw),
],
] + ($raw !== '' ? [CURLOPT_POSTFIELDS => $raw] : []));
$resp = curl_exec($ch);
if ($resp === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($resp, true) ?? [];
if ($status !== 200) throw new DtpgWalletException($status, $data);
return $data;
}
/** $rawBody must be the untouched request body (file_get_contents('php://input')). */
public static function verifyWebhook(string $secret, ?string $ts, ?string $signature, string $rawBody): bool
{
if ($ts === null || !ctype_digit($ts) || abs((int) (microtime(true) * 1000) - (int) $ts) > 5 * 60 * 1000) return false;
return hash_equals(hash_hmac('sha256', $ts . "\n" . $rawBody, $secret), strtolower($signature ?? ''));
}
}Usage
List deposits (paginated), create a withdrawal (handling DUPLICATE_REQUEST), receive webhooks:
go
package dtpgwallet
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
)
// Usage: list deposits, create a withdrawal, handle webhooks.
func Example() {
c := &Client{BaseURL: "https://api.dtpg.net", APIKey: os.Getenv("DTPG_API_KEY"), Secret: os.Getenv("DTPG_API_SECRET")}
ctx := context.Background()
// List deposits. Pagination: pass next_page_token as the next page_token; empty means no more pages.
var page struct {
Deposits []map[string]any `json:"deposits"`
NextPageToken string `json:"next_page_token"`
}
_ = c.Do(ctx, "GET", "/v1/deposits", url.Values{"user_id": {"u_10001"}, "page_size": {"50"}}, nil, &page)
// Create a withdrawal. biz_id is the idempotency key; a repeat returns DUPLICATE_REQUEST.
var w map[string]any
err := c.Do(ctx, "POST", "/v1/withdraws", nil, map[string]any{
"user_id": "u_10001", "biz_id": "order-20261109-0001", "chain_code": "TRON", "token_code": "USDT",
"to_address": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", "amount": "12.5",
}, &w)
var apiErr *APIError
if errors.As(err, &apiErr) && apiErr.Reason == "DUPLICATE_REQUEST" {
fmt.Println("already submitted:", apiErr.Metadata["withdrawId"])
}
// Webhook: verify signature → dedupe by event_id → process → return 2xx.
http.HandleFunc("/dtpg/webhook", func(rw http.ResponseWriter, r *http.Request) {
body, err := VerifyWebhook(os.Getenv("DTPG_WEBHOOK_SECRET"), r)
if err != nil {
rw.WriteHeader(http.StatusUnauthorized)
return
}
var ev struct {
EventID string `json:"event_id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
_ = json.Unmarshal(body, &ev)
// switch ev.Type { case "deposit.confirmed": ... case "withdraw.confirmed": ... }
rw.WriteHeader(http.StatusOK)
})
}java
// Usage: list deposits, create a withdrawal. For webhooks, call DtpgWallet.verifyWebhook with the raw body
// (e.g. Spring: @RequestBody byte[] body, @RequestHeader("X-Timestamp"), @RequestHeader("X-Signature")),
// dedupe by event_id, then return 2xx.
import java.util.List;
import java.util.Map;
public class Example {
public static void main(String[] args) throws Exception {
DtpgWallet wallet = new DtpgWallet("https://api.dtpg.net", System.getenv("DTPG_API_KEY"), System.getenv("DTPG_API_SECRET"));
// Pagination: pass next_page_token as page_token; empty means no more pages.
String page = wallet.request("GET", "/v1/deposits", Map.of("user_id", List.of("u_10001"), "page_size", List.of("50")), null);
System.out.println(page);
// biz_id is the idempotency key; a repeat returns 409 DUPLICATE_REQUEST.
String body = """
{"user_id":"u_10001","biz_id":"order-20261109-0001","chain_code":"TRON","token_code":"USDT",\
"to_address":"TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf","amount":"12.5"}""";
try {
System.out.println(wallet.request("POST", "/v1/withdraws", null, body));
} catch (DtpgWallet.DtpgWalletException e) {
System.out.println(e.status + " " + e.body);
}
}
}py
"""Usage: list deposits, create a withdrawal, handle webhooks (Flask)."""
import json
import os
from flask import Flask, request
from dtpg_wallet import DtpgWallet, DtpgWalletError, verify_webhook
wallet = DtpgWallet(os.environ["DTPG_API_KEY"], os.environ["DTPG_API_SECRET"])
# Pagination: pass next_page_token as page_token; empty means no more pages.
token = None
while True:
page = wallet.request("GET", "/v1/deposits", query={"user_id": "u_10001", "page_size": 50, "page_token": token})
for d in page.get("deposits", []):
print(d["deposit_id"], d.get("status"), d.get("amount"))
token = page.get("next_page_token")
if not token:
break
# biz_id is the idempotency key; a repeat returns DUPLICATE_REQUEST.
try:
w = wallet.request("POST", "/v1/withdraws", body={
"user_id": "u_10001", "biz_id": "order-20261109-0001", "chain_code": "TRON",
"token_code": "USDT", "to_address": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", "amount": "12.5",
})
print(w["withdraw_id"], w.get("status"))
except DtpgWalletError as e:
if e.reason != "DUPLICATE_REQUEST":
raise
print("already submitted:", e.metadata.get("withdrawId"))
app = Flask(__name__)
@app.post("/dtpg/webhook")
def webhook():
raw = request.get_data() # raw bytes, not request.json
if not verify_webhook(os.environ["DTPG_WEBHOOK_SECRET"], request.headers.get("X-Timestamp"),
request.headers.get("X-Signature"), raw):
return "", 401
event = json.loads(raw)
# Dedupe by event["event_id"], then switch on event["type"]: deposit.confirmed / withdraw.confirmed / ...
return "", 200js
// Usage: list deposits, create a withdrawal, handle webhooks (Express).
import express from 'express'
import { DtpgWallet, DtpgWalletError, verifyWebhook } from './dtpg-wallet.mjs'
const wallet = new DtpgWallet({ apiKey: process.env.DTPG_API_KEY, secret: process.env.DTPG_API_SECRET })
// Pagination: pass next_page_token as page_token; empty means no more pages.
let pageToken = ''
do {
const page = await wallet.request('GET', '/v1/deposits', { query: { user_id: 'u_10001', page_size: 50, page_token: pageToken || undefined } })
for (const d of page.deposits ?? []) console.log(d.deposit_id, d.status, d.amount)
pageToken = page.next_page_token ?? ''
} while (pageToken)
// biz_id is the idempotency key; a repeat returns DUPLICATE_REQUEST.
try {
const w = await wallet.request('POST', '/v1/withdraws', {
body: { user_id: 'u_10001', biz_id: 'order-20261109-0001', chain_code: 'TRON', token_code: 'USDT', to_address: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', amount: '12.5' },
})
console.log(w.withdraw_id, w.status)
} catch (e) {
if (e instanceof DtpgWalletError && e.reason === 'DUPLICATE_REQUEST') console.log('already submitted:', e.metadata.withdrawId)
else throw e
}
// Webhook: keep the raw body for signature verification.
const app = express()
app.post('/dtpg/webhook', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyWebhook(process.env.DTPG_WEBHOOK_SECRET, req.headers, req.body)) return res.sendStatus(401)
const event = JSON.parse(req.body.toString('utf8'))
// Dedupe by event.event_id, then switch on event.type: deposit.confirmed / withdraw.confirmed / ...
res.sendStatus(200)
})
app.listen(3000)php
<?php
// Usage: list deposits, create a withdrawal (CLI), or handle a webhook (web request).
require __DIR__ . '/DtpgWallet.php';
if (PHP_SAPI !== 'cli') {
// Webhook endpoint: verify → dedupe by event_id → process → 2xx.
$raw = file_get_contents('php://input');
if (!DtpgWallet::verifyWebhook(getenv('DTPG_WEBHOOK_SECRET'), $_SERVER['HTTP_X_TIMESTAMP'] ?? null, $_SERVER['HTTP_X_SIGNATURE'] ?? null, $raw)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// switch ($event['type']) { case 'deposit.confirmed': ...; case 'withdraw.confirmed': ...; }
http_response_code(200);
exit;
}
$wallet = new DtpgWallet(getenv('DTPG_API_KEY'), getenv('DTPG_API_SECRET'));
// Pagination: pass next_page_token as page_token; empty means no more pages.
$token = null;
do {
$page = $wallet->request('GET', '/v1/deposits', ['user_id' => 'u_10001', 'page_size' => 50, 'page_token' => $token]);
foreach ($page['deposits'] ?? [] as $d) echo $d['deposit_id'], ' ', $d['status'] ?? '', ' ', $d['amount'] ?? '', PHP_EOL;
$token = $page['next_page_token'] ?? null;
} while ($token);
// biz_id is the idempotency key; a repeat returns DUPLICATE_REQUEST.
try {
$w = $wallet->request('POST', '/v1/withdraws', [], [
'user_id' => 'u_10001', 'biz_id' => 'order-20261109-0001', 'chain_code' => 'TRON',
'token_code' => 'USDT', 'to_address' => 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', 'amount' => '12.5',
]);
echo $w['withdraw_id'], ' ', $w['status'] ?? '', PHP_EOL;
} catch (DtpgWalletException $e) {
if ($e->reason() !== 'DUPLICATE_REQUEST') throw $e;
echo 'already submitted: ', $e->metadata()['withdrawId'] ?? '', PHP_EOL;
}Self-test
Check your implementation with these vectors:
| Case | Expected signature |
|---|---|
GET /v1/deposits, query user_id=u 1, chain_code=TRON, status=PENDING,CONFIRMED, note=a+b! | d474bb4c8ff89a37f214d8c39c3d17a502d54b64c1822024a86480c248bcbc86 |
POST /v1/withdraws, body on the Authentication page | 381b9e20fc9b2e0cd97ecb003686acd472db596757b674e56df9e038d708867c |
Webhook, secret whsec-test | 31c194e933a5f62c752bace322b571f8e281edf589c97c5b672ac245e8a54664 |
API secret test-secret, X-Timestamp 1762650000000, X-Nonce 0123456789abcdef0123.