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" }
}
}