Send payouts to Haiti,
programmatically.
The ERTHA Partner API lets your platform deliver money to any recipient in Haiti in Gourdes (HTG) with a single signed request. Prepaid balance, sandbox testing, idempotent retries, and webhooks — built for businesses that move money at scale.
https://api.erthapay.com
From zero to first payout
Every account starts in Sandbox, where a fake balance lets you build and test with zero real money moving.
Create an account
Sign up for a partner account and receive your Sandbox key pair instantly.
Sign a request
Sign each call with HMAC-SHA256 using your secret. No secret ever leaves your server.
Go live
Fund your balance and switch to your Live keys. Your account manager enables Live payouts.
Signing requests
Requests are authenticated with an HMAC-SHA256 signature — there are no bearer tokens and your secret is never transmitted. Each request carries three headers:
| Header | Value |
|---|---|
| x-partner-key | Your public key (pk_test_… / pk_live_…) |
| x-partner-timestamp | Current time in milliseconds. Must be within ±5 minutes. |
| x-partner-signature | Hex HMAC-SHA256 of the signing string, keyed by your secret. |
The signing string
Concatenate the timestamp, the HTTP method and path, and the raw request body — exactly as shown. For requests with no body (e.g. GET), the body is empty, so the string ends in a trailing dot.
Keep secrets in the environment. Read your key and secret from environment variables — never hard-code a secret in your source or ship it to a browser.
# signing string = "{ts}.{METHOD} {path}.{body}" TS=$(($(date +%s)*1000)) BODY='{"recipient":"+509XXXXXXXX","amount":1000,"reference":"order-1001"}' MSG="$TS.POST /transfers.$BODY" SIG=$(printf '%s' "$MSG" | openssl dgst -sha256 \ -hmac "$ERTHA_API_SECRET" | sed 's/^.* //') curl https://api.erthapay.com/transfers \ -H "content-type: application/json" \ -H "x-partner-key: $ERTHA_API_KEY" \ -H "x-partner-timestamp: $TS" \ -H "x-partner-signature: $SIG" \ -d "$BODY"
import crypto from 'node:crypto'; const ts = Date.now().toString(); const body = JSON.stringify({ recipient:'+509XXXXXXXX', amount:1000, reference:'order-1001' }); const msg = `${ts}.POST /transfers.${body}`; const sig = crypto.createHmac('sha256', process.env.ERTHA_API_SECRET) .update(msg).digest('hex'); await fetch('https://api.erthapay.com/transfers', { method:'POST', headers:{ 'content-type':'application/json', 'x-partner-key':process.env.ERTHA_API_KEY, 'x-partner-timestamp':ts, 'x-partner-signature':sig, }, body, });
import hashlib, hmac, json, os, time import requests ts = str(int(time.time() * 1000)) body = json.dumps({"recipient":"+509XXXXXXXX","amount":1000,"reference":"order-1001"}, separators=(",",":")) msg = f"{ts}.POST /transfers.{body}" sig = hmac.new(os.environ["ERTHA_API_SECRET"].encode(), msg.encode(), hashlib.sha256).hexdigest() requests.post("https://api.erthapay.com/transfers", data=body, headers={ "content-type":"application/json", "x-partner-key":os.environ["ERTHA_API_KEY"], "x-partner-timestamp":ts, "x-partner-signature":sig, })
Sandbox & Live
Two isolated key pairs. Which key signs the request decides the mode — the same endpoints serve both.
pk_test_… / sk_test_…A fake balance and simulated payouts. Nothing real moves — perfect for building and CI. Recipients ending in 0000 simulate an invalid number.
pk_live_… / sk_live_…Real payouts drawn from your prepaid balance. Live delivery is enabled per account by your ERTHA account manager once you're ready.
Create a payout
Sends a payout to a recipient's mobile money number in Haiti. The amount is a whole number of Gourdes. Provide your own reference — it's how you retrieve status later and how retries stay safe.
| Body param | Type | |
|---|---|---|
| recipient | string | Required |
Recipient mobile number in +509XXXXXXXX format. | ||
| amount | integer | Required |
| Amount in whole HTG (Gourdes). Must be a positive integer. | ||
| reference | string | Required |
| Your unique id for this payout. Reuse it to retry safely. | ||
{
"id": "txn_9f3c1a20",
"reference": "order-1001",
"status": "pending",
"amount": 1000,
"currency": "HTG",
"livemode": true
}
Statuses. A payout is pending, then resolves to paid or failed. Poll the retrieve endpoint or subscribe to webhooks.
Retrieve a payout
Fetch the current state of a payout by the reference you created it with (or by its txn_ id). Read-only and safe to poll.
| Path param | Type | |
|---|---|---|
| reference | string | Required |
{
"id": "txn_9f3c1a20",
"reference": "order-1001",
"status": "paid",
"amount": 1000,
"currency": "HTG"
}
Validate a recipient
Confirm a recipient number resolves to a real account and return the registered name — check before you send, and show your user who they're paying.
| Body param | Type | |
|---|---|---|
| recipient | string | Required |
{
"recipient": "+509XXXXXXXX",
"name": "Jean Baptiste",
"livemode": true
}
Get balance
Your available prepaid balance, in Gourdes. In Sandbox this returns your fake test balance.
{
"balance": 96379,
"currency": "HTG",
"livemode": true
}
Idempotency
Networks fail. Retries shouldn't cost money twice.
Every payout carries your reference. If a request times out, re-send the same reference with the same body — you'll get the original payout back, never a duplicate. Re-using a reference with a different amount or recipient is rejected with 409 DUPLICATE_REQUEST, so a mistaken reuse can't silently overwrite a real payment.
Errors & status codes
Errors return a JSON body of the shape { "error": "CODE" }. Never surface a raw error to your end user — map it to your own copy.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Bad signature, wrong key, or a timestamp outside the ±5-minute window. |
| 400 | INVALID_REQUEST | Missing or unknown fields in the body. |
| 400 | INVALID_AMOUNT | Amount is not a positive whole number of HTG. |
| 400 | REFERENCE_REQUIRED | A signed reference is required to create a payout. |
| 409 | DUPLICATE_REQUEST | Reference reused with a different payload. |
| 429 | RATE_LIMITED | Too many requests — back off and retry after the returned delay. |
| 503 | SERVICE_UNAVAILABLE | Temporary — safe to retry with the same reference. |