API documentation

Ship integrations
in an afternoon.

Single REST API. HMAC-signed. Idempotent. Works the way you'd expect from a modern payment stack.

Base URL

https://your-domain.com/v1

Authentication

Every request is signed with HMAC-SHA256. Include three headers:

X-API-Key: pk_test_xxxxxxxxxxxxxxxx X-Timestamp: 1730712345 X-Signature: <base64-hmac> X-Api-Secret: sk_test_xxxxxxxxxxxxxxxx # sandbox only

Compute the signature as:

stringToSign = timestamp + "\n" + METHOD + "\n" + path + "\n" + sha256_hex(body) signature = base64(hmac_sha256(api_secret, stringToSign))

Create a deposit

# POST /v1/deposits { "amount": "10.00", "currency": "USD", "channel": "evc", "customer": { "phone": "252611234567", "name": "Ahmed M." }, "merchant_reference": "order_12345", "description": "Order #12345" } # 201 Created { "id": "hdx_dep_01HKL3...", "reference": "HDX-D-A1B2C3D4", "status": "pending", "amount": "10.00", "net_amount": "9.85", "fee": "0.15", "channel": "evc", "created_at": "2026-05-06T..." }

Webhooks

When a transaction state changes, we POST to your webhook URL with this header:

HubdexPay-Signature: t=1730712345,v1=<base64-hmac> HubdexPay-Event: transaction.completed

Verify with your webhook secret (visible in dashboard → Webhooks):

expected = hmac_sha256(webhook_secret, t + "." + raw_body) if (!hash_equals(expected, v1)) reject;

Endpoints

POST/v1/deposits GET/v1/deposits/{id} GET/v1/deposits POST/v1/payouts GET/v1/payouts/{id} GET/v1/payouts GET/v1/transactions GET/v1/transactions/{id} GET/v1/balance GET/v1/providers

Errors

Errors return a uniform JSON shape. The HTTP status reflects the kind of error.

{ "error": { "code": "insufficient_balance", "message": "Merchant balance is below the requested payout amount.", "details": { "requested": "100.00", "available": "42.50" } } }