Webhooks
Webhooks are how JKAPay tells your server when a transaction reaches a final state. They're free, real-time, signed, and retried automatically until your server returns a 2xx response.
How delivery works
- You call
POST /v1/payments/initializewith acallbackUrl. The customer is prompted on their phone. - The customer authorises or declines the payment.
- JKAPay normalises the result and signs the payload with
HMAC-SHA256. - JKAPay POSTs the signed payload to your
callbackUrl. - Your endpoint returns
2xx→ delivered. Any other status or a network error → JKAPay retries with exponential backoff up to 8 times.
One callback per final state. JKAPay dedupes upstream retries — if the mobile network sends us the same
SUCCESS twice, you receive exactly one webhook.Request headers JKAPay sends
Headers
Content-Type
string
application/jsonUser-Agent
string
JKAPay-WebhooksX-JKAPay-Signature
string
The HMAC-SHA256 digest of the request body. Format:
v1=<hex digest>. See Verifying webhook signatures.X-JKAPay-Timestamp
string
Unix seconds when JKAPay signed the payload. Used for replay-attack protection.
X-JKAPay-Key-Id
string
The
pk_… id of the API key whose secret was used to sign this delivery. Useful when you've issued multiple keys.Payload
HTTP
POST /webhooks/jkapay HTTP/1.1
Host: webhooks.example.com
Content-Type: application/json
User-Agent: JKAPay-Webhooks
X-JKAPay-Signature: v1=58ab8e9c…
X-JKAPay-Timestamp: 1748192400
X-JKAPay-Key-Id: pk_test_xxxxx
{
"createdAt": "2026-05-25T16:30:18.213Z",
"data": {
"reference": "JKA_M3A8_8F2E1B4C",
"clientReference": "ORD-12345",
"status": "SUCCESS",
"amount": "1.00",
"fee": "0.02",
"net": "0.98",
"currency": "GHS",
"customer": { "msisdn": "233244000000", "network": "MTN" },
"description": "Order #12345",
"metadata": { "orderId": "12345" },
"completedAt": "2026-05-25T16:30:18.213Z"
}
}Payload fields
Body
createdAt
string (ISO 8601)
When JKAPay generated this callback. Distinct from
data.completedAt (which is when the transaction settled).data.reference
string
JKAPay's reference for the transaction. The primary key on your side should be this.
data.clientReference
string | null
The
merchantRef you sent at initialize. Use to reconcile against your own system.data.status
string
Either
SUCCESS or FAILED. The transaction's final state. Branch on this — there is no separate event field.data.amount
string
Transaction amount as a GHS string.
data.fee
string
Platform fee deducted on success.
"0.00" on failed transactions.data.net
string
Amount you receive after the platform fee.
data.currency
string
Always
GHS today.data.customer
object
The customer's
msisdn (normalised) and network. The customer's resolved name is intentionally not included on the wire — it's still visible in your dashboard.data.description
string
The description you supplied at initialize.
data.metadata
object | null
The metadata you supplied at initialize, echoed back unchanged.
data.completedAt
string (ISO 8601)
When the transaction reached its final state.
data.sandbox
boolean (optional)
Present and
true only on test-mode deliveries. Use to gate any fulfilment / settlement step in shared environments. See Sandbox / test mode.Branching on outcome. Inspect
data.status: SUCCESS means money has moved; FAILED means it hasn't and won't (the customer declined, timed out, had insufficient funds, or the network rejected the prompt).Test-key callbacks carry a sandbox flag. When the originating request used a
sk_test_… key, the payload includes data.sandbox: true and data.customer.network: "SANDBOX". Any fulfilment step that ships goods, settles funds, or notifies a real customer should refuse to act when data.sandbox === true. See Sandbox / test mode.Best practices
- Always verify the signature. See Verifying webhook signatures. Treat unsigned or invalid deliveries as if they never happened.
- Respond 2xx fast. Acknowledge first, do the heavy work async (queue it). JKAPay times out delivery after 15 seconds.
- Be idempotent. Even with our dedupe, network conditions can cause repeats. Key your processing on
data.reference. - Handle out-of-order events. Compare
X-JKAPay-Timestampagainst your stored value — older timestamps mean a stale delivery you've already processed. - Don't trust the source IP. JKAPay doesn't publish a stable egress IP. Use the signature, not network-layer filtering.
Retry schedule
If your endpoint returns non-2xx or times out, JKAPay retries on this schedule:
- Attempt 1: immediate
- Attempt 2: +5 seconds
- Attempt 3: +10 seconds
- Attempt 4: +20 seconds
- Attempt 5: +40 seconds
- Attempt 6: +80 seconds
- Attempt 7: +160 seconds
- Attempt 8: +320 seconds
After 8 failed attempts the delivery is marked FAILED. Every attempt is visible in the Webhooks dashboard, where you can replay deliveries by hand.