API Reference
Complete documentation of every endpoint in the HexAI Payment Gateway API. All endpoints are prefixed with /api/v1.
Base URL
One base URL serves both environments — your API key prefix selects which one you hit. There is no separate sandbox host to switch to.
https://api.hpg.hexai.gm/api/v1sk_test_…Sandbox — no real money moves; magic amounts trigger test outcomes.
sk_live_…Production — real transactions against live rails.
Integrating each rail
HPG routes each payment to one of several underlying rails. Choose a rail per request with the optional provider field — omit it and HPG defaults to WAVE; an unknown or disabled provider returns 422 provider_not_enabled. The rails do not integrate the same way: Wave and Waychit redirect the customer, while APS is a two-step wallet + OTP charge with no redirect. Pick a rail below for its exact end-to-end flow.
- 1
Create the collection
Call
/collections/initiate.provideris optional — it defaults toWAVE. You get back aredirect_urland aPENDINGstatus.Request
curl -X POST https://api.hpg.hexai.gm/api/v1/collections/initiate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "amount": "10.50", "currency": "GMD", "client_reference": "ORD-12345", "customer_name": "Awa Ceesay", "customer_mobile": "+2207123456", "success_url": "https://yourapp.com/success", "error_url": "https://yourapp.com/failed" }'200Response
{ "status": "success", "data": { "transaction_id": "b3f1c2a4-5d6e-7f80-9a1b-2c3d4e5f6a7b", "client_reference": "ORD-12345", "amount": "10.50", "currency": "GMD", "status": "PENDING", "provider": "WAVE", "redirect_url": "https://checkout.wave.com/c/cos-abc123", "next_action": null, "created_at": "2026-07-28T12:00:00.000Z" } } - 2
Send the customer to Wave
Redirect the browser to
data.redirect_url. The customer approves the payment in Wave, then returns to yoursuccess_url/error_url. - 3
Get the final result
The redirect is not proof of payment. Trust the
payment.succeeded/payment.failedwebhook, or pollGET /collections/status/{client_reference}.
POST /payouts/send.Sandbox Mode
Sandbox is the same API with fake money behind it — same endpoints, same responses, same webhooks — so your integration works unchanged when you go live. Reach it with a test key (sk_test_*), or flip your dashboard to Test mode with the toggle in the header to watch the same transactions arrive as you make them.
Magic amounts force a specific outcome, and the conventions are shared across every rail (Wave, APS and Waychit), so a sandbox suite written once behaves the same whichever provider you set:
| Amount (GMD) | Outcome | Simulated Behavior |
|---|---|---|
| 100.00 | SUCCESS | Transaction completes successfully |
| 200.00 | PENDING | Transaction stays pending (test polling) |
| 400.00 | FAILED | Insufficient funds error |
| 403.00 | FAILED | Account blocked error |
| 408.00 | EXPIRED | Checkout session times out before the customer pays |
| 429.00 | FAILED | Rate limit exceeded |
| 499.00 | CANCELLED | Customer abandons the checkout |
| 500.00 | ERROR | Internal server error (test retries) |
EXPIRED and CANCELLED are not failures. Nothing was charged and nothing was refused — you simply lost the customer, so re-offering the same checkout is a valid next step. A FAILED collection was actually declined. The amounts echo the HTTP status they stand for, so the table is guessable: 408 Request Timeout, 499 client closed request.
Payouts draw on a per-rail float you fund yourself in Sandbox → Rail floats. A payout larger than the float is held as AWAITING_LIQUIDITY rather than failed — exactly what happens live when a rail is short.
Webhooks
HPG sends a single, provider-agnostic webhook to your endpoint whenever a transaction status changes — the payload is identical whether the payment ran over Wave, APS, or Waychit. This is especially important for APS and WAYCHIT_CARD, which have no redirect/polling flow: the webhook is your primary signal that money moved.
Payload Example
{
"event": "payment.succeeded",
"data": {
"id": "tx_abc123",
"type": "collection",
"reference": "INV-001",
"amount": "500.00",
"currency": "GMD",
"status": "SUCCEEDED",
"provider": "WAVE",
"environment": "LIVE",
"completed_at": "2026-01-01T12:00:00.000Z"
}
}Headers HPG sends
x-hexai-signature— HMAC-SHA256 of the raw JSON body, keyed with your webhook secretwave-signature— same value, kept as a legacy alias for existing Wave-style consumersX-Hexai-Event— the event type (e.g.payment.succeeded)X-Hexai-Sandbox— present andtrueonly for sandbox traffic
Event types
These are every event HPG sends. Switch on event — names are past-tense resource.event and will not change. Note that a lost customer (expired, cancelled) is reported separately from a decline (failed), because only one of them is worth retrying as-is.
payment.succeeded— a collection settledpayment.failed— a collection was declinedpayment.expired— the checkout session timed out before the customer acted; nothing was chargedpayment.cancelled— the customer abandoned the checkout; nothing was chargedpayout.succeeded— a payout reached the recipientpayout.failed— a payout could not be deliveredpayout.pending— a payout is in flight, awaiting settlementpayout.reversed— a payout was reversed within the 3-day window
Verify x-hexai-signature by recomputing the HMAC-SHA256 of the exact request body with your webhook secret and comparing in constant time before you act on the event. The wave-signature header carries the same value, so existing verifiers keep working unchanged.
Error Handling
The API uses standard HTTP status codes and returns errors in a consistent JSON format.
| Status Code | Meaning | Common Causes |
|---|---|---|
| 200 | Success | Request completed successfully |
| 400 | Bad Request | Invalid parameters, missing required fields, malformed JSON |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Valid API key but insufficient permissions |
| 404 | Not Found | Resource doesn't exist (invalid transaction ID, etc.) |
| 429 | Rate Limited | Too many requests in a short time |
| 500 | Internal Error | Server-side issue (retry with exponential backoff) |
| 503 | Service Unavailable | Temporary maintenance or overload |
Error Response Format
All error responses follow this structure:
{
"status": "error",
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Your account balance is insufficient for this payout.",
"details": {
"required_amount_bututs": 50000,
"available_balance_bututs": 30000
}
}
}Verification Examples
# Verify manually using openssl
echo -n '{"event":"payment.succeeded",...}' | \
openssl dgst -sha256 -hmac "YOUR_WEBHOOK_SECRET"