Download OpenAPI specification:
PayKunda is API infrastructure for emerging markets. One integration lets you send money to mobile money wallets, bank accounts and cash pickup points, and sell airtime, data bundles and prepaid electricity.
The API is organised around REST. It has predictable resource-oriented URLs, accepts and returns JSON, and uses standard HTTP response codes and verbs.
Base URL
https://api.paykunda.com/v3
Authenticate with your secret API key in the Authorization header:
curl https://api.paykunda.com/v3/balances \
-H "Authorization: Bearer sk_live_9fL2mQz8Kx7Yw3kQ2"
sk_test_… keys hit the sandbox — identical behaviour, simulated settlement.sk_live_… keys move real money.payouts:write), set an expiry, roll it, or revoke it
instantly.All amounts are decimal strings in the major units of an ISO-4217
currency — {"amount": "1500.00", "currency": "NGN"} is ₦1,500.00.
Amounts are strings (never JSON floats) to avoid floating-point rounding
errors; include two decimal places, e.g. "100.00".
Pass an Idempotency-Key header (any unique string, e.g. a UUID) on every
POST. Retrying with the same key returns the original response instead
of performing the action twice — a network timeout can never double-send money.
curl https://api.paykunda.com/v3/payouts \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: 0b7c9d4e-1f2a-4b3c-8d5e-6f7a8b9c0d1e" \
-d '{...}'
PayKunda returns a consistent error envelope. The code is machine-readable
and stable; message is for humans; param names the offending field.
{
"error": {
"type": "invalid_request_error",
"code": "amount_below_minimum",
"message": "Amount must be at least NGN 100.00.",
"param": "amount",
"doc_url": "https://docs.paykunda.com/errors#amount_below_minimum",
"request_id": "req_7Hs92kL"
}
}
| Status | Type | Meaning |
|---|---|---|
| 400 | invalid_request_error |
Malformed or invalid parameters |
| 401 | authentication_error |
Bad key, revoked key, or IP not allowed |
| 402 | insufficient_funds_error |
Balance too low for the operation |
| 403 | permission_error |
Key lacks the required scope |
| 404 | invalid_request_error |
Resource does not exist |
| 409 | idempotency_error |
Same idempotency key, different payload |
| 429 | rate_limit_error |
Too many requests — honour Retry-After |
| 5xx | api_error |
Something failed on our side — safe to retry with the same idempotency key |
All list endpoints are cursor-paginated:
GET /v3/payouts?limit=25&starting_after=po_9Ks82mQz
Responses have the shape {"object": "list", "data": [...], "has_more": true}.
Register webhook endpoints to receive events (payout.paid,
electricity_token.delivered, …). Every delivery is signed:
Webhook-Signature: t=1754901120,v1=5257a869e7…
v1 = HMAC_SHA256(signing_secret, t + "." + raw_body). Reject deliveries
where |now - t| > 5 minutes. As a financial best practice, treat webhooks
as notifications — confirm by fetching the resource by id.
| object | string Value: "list" |
| has_more | boolean |
Array of objects (Balance) |
{- "object": "list",
- "data": [
- {
- "object": "balance",
- "wallet": "wal_2Fj81mK",
- "currency": "USD",
- "available": "12500.00",
- "pending": "300.00"
}, - {
- "object": "balance",
- "wallet": "wal_9Xk20aQ",
- "currency": "NGN",
- "available": "845000.00",
- "pending": "0.00"
}
], - "has_more": false
}Send money to mobile money wallets, bank accounts, or cash pickup points. Creating a payout debits your wallet for the amount plus fee immediately.
A payout moves through a simple lifecycle:
pending → processing → paid — or failed (funds are returned to your
wallet; a payout is never left in limbo).
Debits your wallet for amount plus the fee and sends the money to the
given destination. By default the wallet matching currency is
debited; pass wallet (ids from GET /balances) to debit a specific
wallet/account. A 402 insufficient_funds error is returned if the
balance cannot cover amount + fee. Always pass an Idempotency-Key
header.
The recipient is described by destination.holder (name, contact,
address) plus the account details for the destination type. The
sender carries the originator's compliance details — some corridors
additionally require purpose, source_of_funds, or the sender's
address and identity document (see GET /payout-providers).
| Idempotency-Key required | string Example: 0b7c9d4e-1f2a-4b3c-8d5e-6f7a8b9c0d1e Unique per logical request. Retries with the same key return the original response. |
| amount required | string Decimal amount, e.g. "100.00". Debited from your wallet along with the fee. |
| currency required | string Must match a wallet balance you hold. |
required | object (PayoutDestination) For |
required | object (PayoutSender) The originating party (compliance). |
| wallet | string Optional — the wallet/account to debit (ids from |
| purpose | string Enum: "goods_purchase" "services_payment" "invoice_payment" "loan_repayment" "bills_payment" "salary_and_wages" "p2p_transfer" "remittance" "donation" "travel_and_accommodation" "tax_payment" "insurance_premium" Why the money is being sent. Required in some corridors. |
| source_of_funds | string Enum: "salary" "business_income" "savings" "pension" "investment_income" "inheritance" "gift" "loan" "asset_sale" "government" "ngo" "other" Where the money came from. Required in some corridors. |
| reference | string Your reference; shown to the recipient where the corridor supports it. |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. |
| id | string |
| object | string Value: "payout" |
| status | string Enum: "pending" "processing" "paid" "failed" "returned"
|
| amount | string Decimal amount. |
| currency | string |
| wallet | string The wallet that was debited. |
object Debited from your wallet together with | |
object (PayoutDestination) For | |
object (PayoutSender) The originating party (compliance). | |
| purpose | string or null |
| source_of_funds | string or null |
| reference | string |
| withdrawal_code | string or null Cash pickup only — the code the recipient presents at the till. |
object or null | |
| estimated_delivery | string or null <date-time> |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. | |
| created_at | string <date-time> |
{- "amount": "500000.00",
- "currency": "NGN",
- "destination": {
- "type": "MOBILE_MONEY",
- "country": "NG",
- "holder": {
- "type": "individual",
- "name": "Ebrima Saidy",
- "msisdn": "+2348012345678"
}, - "mobile_money": {
- "provider": "mtn_ng",
- "msisdn": "+2348012345678"
}
}, - "sender": {
- "type": "individual",
- "name": "Pa Essa Jabang",
- "country": "GB",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}, - "contact": {
- "email": "essa@example.com",
- "msisdn": "+447700900123"
}, - "date_of_birth": "1988-04-12",
- "identity_document": {
- "type": "passport",
- "number": "PC1234567",
- "country": "GB"
}
}, - "purpose": "remittance",
- "source_of_funds": "salary",
- "reference": "INV-2031",
- "metadata": {
- "order_id": "9912"
}
}{- "id": "po_9Ks82mQz",
- "object": "payout",
- "status": "pending",
- "amount": "500000.00",
- "currency": "NGN",
- "wallet": "wal_9Xk20aQ",
- "fee": {
- "amount": "2500.00",
- "currency": "NGN"
}, - "destination": {
- "type": "MOBILE_MONEY",
- "country": "NG",
- "holder": {
- "type": "individual",
- "name": "Ebrima Saidy",
- "msisdn": "+2207834567",
- "email": "ebrima@example.com",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}
}, - "mobile_money": {
- "provider": "mtn_ng",
- "msisdn": "+2348012345678"
}, - "bank_account": {
- "bank_code": "058",
- "account_number": "0123456789"
}
}, - "sender": {
- "type": "individual",
- "name": "Pa Essa Jabang",
- "country": "GB",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}, - "contact": {
- "email": "essa@example.com",
- "msisdn": "+447700900123"
}, - "date_of_birth": "1988-04-12",
- "identity_document": {
- "type": "passport",
- "number": "PC1234567",
- "country": "GB",
- "issued_on": "2021-03-01",
- "expires_on": "2031-03-01"
}, - "business_registration": {
- "number": "C-102934",
- "country": "GM"
}
}, - "purpose": "remittance",
- "source_of_funds": "salary",
- "reference": "INV-2031",
- "withdrawal_code": "PK-4829-1173",
- "failure": {
- "code": "recipient_account_blocked",
- "message": "string"
}, - "estimated_delivery": "2019-08-24T14:15:22Z",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}| limit | integer [ 1 .. 100 ] Default: 25 |
| starting_after | string Cursor — the |
| status | string Enum: "pending" "processing" "paid" "failed" "returned" |
| destination_type | string Enum: "MOBILE_MONEY" "BANK" "CASH_PICKUP" |
| object | string Value: "list" |
| has_more | boolean |
Array of objects (Payout) |
{- "object": "list",
- "has_more": true,
- "data": [
- {
- "id": "po_9Ks82mQz",
- "object": "payout",
- "status": "pending",
- "amount": "500000.00",
- "currency": "NGN",
- "wallet": "wal_9Xk20aQ",
- "fee": {
- "amount": "2500.00",
- "currency": "NGN"
}, - "destination": {
- "type": "MOBILE_MONEY",
- "country": "NG",
- "holder": {
- "type": "individual",
- "name": "Ebrima Saidy",
- "msisdn": "+2207834567",
- "email": "ebrima@example.com",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}
}, - "mobile_money": {
- "provider": "mtn_ng",
- "msisdn": "+2348012345678"
}, - "bank_account": {
- "bank_code": "058",
- "account_number": "0123456789"
}
}, - "sender": {
- "type": "individual",
- "name": "Pa Essa Jabang",
- "country": "GB",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}, - "contact": {
- "email": "essa@example.com",
- "msisdn": "+447700900123"
}, - "date_of_birth": "1988-04-12",
- "identity_document": {
- "type": "passport",
- "number": "PC1234567",
- "country": "GB",
- "issued_on": "2021-03-01",
- "expires_on": "2031-03-01"
}, - "business_registration": {
- "number": "C-102934",
- "country": "GM"
}
}, - "purpose": "remittance",
- "source_of_funds": "salary",
- "reference": "INV-2031",
- "withdrawal_code": "PK-4829-1173",
- "failure": {
- "code": "recipient_account_blocked",
- "message": "string"
}, - "estimated_delivery": "2019-08-24T14:15:22Z",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}
]
}| payout_id required | string Example: po_9Ks82mQz |
| id | string |
| object | string Value: "payout" |
| status | string Enum: "pending" "processing" "paid" "failed" "returned"
|
| amount | string Decimal amount. |
| currency | string |
| wallet | string The wallet that was debited. |
object Debited from your wallet together with | |
object (PayoutDestination) For | |
object (PayoutSender) The originating party (compliance). | |
| purpose | string or null |
| source_of_funds | string or null |
| reference | string |
| withdrawal_code | string or null Cash pickup only — the code the recipient presents at the till. |
object or null | |
| estimated_delivery | string or null <date-time> |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. | |
| created_at | string <date-time> |
{- "id": "po_9Ks82mQz",
- "object": "payout",
- "status": "pending",
- "amount": "500000.00",
- "currency": "NGN",
- "wallet": "wal_9Xk20aQ",
- "fee": {
- "amount": "2500.00",
- "currency": "NGN"
}, - "destination": {
- "type": "MOBILE_MONEY",
- "country": "NG",
- "holder": {
- "type": "individual",
- "name": "Ebrima Saidy",
- "msisdn": "+2207834567",
- "email": "ebrima@example.com",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}
}, - "mobile_money": {
- "provider": "mtn_ng",
- "msisdn": "+2348012345678"
}, - "bank_account": {
- "bank_code": "058",
- "account_number": "0123456789"
}
}, - "sender": {
- "type": "individual",
- "name": "Pa Essa Jabang",
- "country": "GB",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}, - "contact": {
- "email": "essa@example.com",
- "msisdn": "+447700900123"
}, - "date_of_birth": "1988-04-12",
- "identity_document": {
- "type": "passport",
- "number": "PC1234567",
- "country": "GB",
- "issued_on": "2021-03-01",
- "expires_on": "2031-03-01"
}, - "business_registration": {
- "number": "C-102934",
- "country": "GM"
}
}, - "purpose": "remittance",
- "source_of_funds": "salary",
- "reference": "INV-2031",
- "withdrawal_code": "PK-4829-1173",
- "failure": {
- "code": "recipient_account_blocked",
- "message": "string"
}, - "estimated_delivery": "2019-08-24T14:15:22Z",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}Only possible while status is pending. Funds are released back to your balance.
| payout_id required | string |
| id | string |
| object | string Value: "payout" |
| status | string Enum: "pending" "processing" "paid" "failed" "returned"
|
| amount | string Decimal amount. |
| currency | string |
| wallet | string The wallet that was debited. |
object Debited from your wallet together with | |
object (PayoutDestination) For | |
object (PayoutSender) The originating party (compliance). | |
| purpose | string or null |
| source_of_funds | string or null |
| reference | string |
| withdrawal_code | string or null Cash pickup only — the code the recipient presents at the till. |
object or null | |
| estimated_delivery | string or null <date-time> |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. | |
| created_at | string <date-time> |
{- "id": "po_9Ks82mQz",
- "object": "payout",
- "status": "pending",
- "amount": "500000.00",
- "currency": "NGN",
- "wallet": "wal_9Xk20aQ",
- "fee": {
- "amount": "2500.00",
- "currency": "NGN"
}, - "destination": {
- "type": "MOBILE_MONEY",
- "country": "NG",
- "holder": {
- "type": "individual",
- "name": "Ebrima Saidy",
- "msisdn": "+2207834567",
- "email": "ebrima@example.com",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}
}, - "mobile_money": {
- "provider": "mtn_ng",
- "msisdn": "+2348012345678"
}, - "bank_account": {
- "bank_code": "058",
- "account_number": "0123456789"
}
}, - "sender": {
- "type": "individual",
- "name": "Pa Essa Jabang",
- "country": "GB",
- "address": {
- "line_1": "22 Coldharbour Lane",
- "line_2": "string",
- "line_3": "string",
- "city": "London",
- "postal_code": "SE5 9PR",
- "country": "GB"
}, - "contact": {
- "email": "essa@example.com",
- "msisdn": "+447700900123"
}, - "date_of_birth": "1988-04-12",
- "identity_document": {
- "type": "passport",
- "number": "PC1234567",
- "country": "GB",
- "issued_on": "2021-03-01",
- "expires_on": "2031-03-01"
}, - "business_registration": {
- "number": "C-102934",
- "country": "GM"
}
}, - "purpose": "remittance",
- "source_of_funds": "salary",
- "reference": "INV-2031",
- "withdrawal_code": "PK-4829-1173",
- "failure": {
- "code": "recipient_account_blocked",
- "message": "string"
}, - "estimated_delivery": "2019-08-24T14:15:22Z",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}Name-enquiry before sending — resolve a bank account or a mobile money wallet to its registered account holder, so you can confirm it belongs to the person you expect. Provide the block matching type.
| type required | string Enum: "BANK" "MOBILE_MONEY" |
| country required | string ISO 3166 alpha-2. |
| provider required | string Bank code (BANK) or mobile money provider (MOBILE_MONEY). From GET /payout-providers. |
| account_number required | string Bank account number (BANK) or msisdn (MOBILE_MONEY). |
{- "type": "BANK",
- "country": "NG",
- "provider": "058",
- "account_number": "0123456789"
}{- "object": "payout_account",
- "type": "BANK",
- "country": "NG",
- "provider": "058",
- "account_number": "0123456789",
- "account_name": "EBRIMA SAIDY"
}| country | string Example: country=NG |
| type | string Enum: "MOBILE_MONEY" "BANK" "CASH_PICKUP" |
{- "object": "list",
- "data": [
- {
- "object": "payout_provider",
- "id": "mtn_ng",
- "name": "MTN Mobile Money",
- "type": "MOBILE_MONEY",
- "country": "NG",
- "currency": "NGN",
- "min_amount": "100.00",
- "max_amount": "5000000.00",
- "required_fields": [
- "destination.holder.name",
- "destination.mobile_money.msisdn",
- "sender.name",
- "sender.country",
- "sender.address",
- "purpose"
]
}
], - "has_more": false
}The mobile network is auto-detected from the number. Pass an Idempotency-Key.
| Idempotency-Key required | string Example: 0b7c9d4e-1f2a-4b3c-8d5e-6f7a8b9c0d1e Unique per logical request. Retries with the same key return the original response. |
| msisdn required | string |
| amount required | string Decimal amount, e.g. "100.00". |
| currency required | string |
| wallet | string Optional — the wallet to debit. Defaults to your wallet in |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. |
| id | string |
| object | string Value: "airtime_topup" |
| status | string Enum: "processing" "delivered" "failed" |
| msisdn | string |
| provider | string |
| amount | string Decimal amount. |
| currency | string |
| wallet | string The wallet that was debited. |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. | |
| created_at | string <date-time> |
{- "msisdn": "+2207834567",
- "amount": "100.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}
}{- "id": "at_7Hs92k",
- "object": "airtime_topup",
- "status": "processing",
- "msisdn": "+2207834567",
- "provider": "africell_gm",
- "amount": "100.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}| limit | integer [ 1 .. 100 ] Default: 25 |
| starting_after | string Cursor — the |
| object | string Value: "list" |
| has_more | boolean |
Array of objects (AirtimeTopup) |
{- "object": "list",
- "has_more": true,
- "data": [
- {
- "id": "at_7Hs92k",
- "object": "airtime_topup",
- "status": "processing",
- "msisdn": "+2207834567",
- "provider": "africell_gm",
- "amount": "100.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}
]
}| country | string Example: country=GM |
{- "object": "list",
- "data": [
- {
- "object": "airtime_provider",
- "id": "africell_gm",
- "name": "Africell",
- "country": "GM",
- "currency": "GMD",
- "min_amount": "5.00",
- "max_amount": "1000.00"
}
], - "has_more": false
}| country | string Example: country=GM |
| msisdn | string Example: msisdn=+2207834567 When given, bundles are filtered to the number's network. |
{- "object": "list",
- "data": [
- {
- "object": "data_bundle_product",
- "code": "africell_gm_1gb_7d",
- "name": "1 GB / 7 days",
- "amount": "50.00",
- "currency": "GMD",
- "provider": "africell_gm"
}
], - "has_more": false
}| Idempotency-Key required | string Example: 0b7c9d4e-1f2a-4b3c-8d5e-6f7a8b9c0d1e Unique per logical request. Retries with the same key return the original response. |
| msisdn required | string |
| bundle required | string |
| wallet | string Optional — the wallet to debit. Defaults to your wallet in the bundle's currency. |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. |
{- "msisdn": "+2207834567",
- "bundle": "africell_gm_1gb_7d",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}
}{- "id": "db_5Qw83nZ",
- "object": "data_bundle",
- "status": "delivered",
- "msisdn": "+2207834567",
- "bundle": "africell_gm_1gb_7d",
- "amount": "50.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "created_at": "2026-08-06T10:12:00Z"
}Always validate before vending — the response shows who the meter belongs to.
| provider required | string |
| meter_number required | string |
{- "provider": "nawec_gm",
- "meter_number": "01234567890"
}{- "object": "meter",
- "provider": "nawec_gm",
- "meter_number": "01234567890",
- "customer_name": "MSJ Bureau De Change Ltd",
- "address": "Sayerrjobe Ave, Serrekunda"
}Vends a prepaid (STS) token. Synchronous providers return the token
immediately; asynchronous ones return status: processing and deliver
the token via electricity_token.delivered webhook + on the resource.
| Idempotency-Key required | string Example: 0b7c9d4e-1f2a-4b3c-8d5e-6f7a8b9c0d1e Unique per logical request. Retries with the same key return the original response. |
| provider required | string |
| meter_number required | string |
| amount required | string Decimal amount, e.g. "500.00". |
| currency required | string |
| wallet | string Optional — the wallet to debit. Defaults to your wallet in |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. |
| id | string |
| object | string Value: "electricity_token" |
| status | string Enum: "processing" "delivered" "failed" |
| provider | string |
| meter_number | string |
| token | string or null The STS token. Null while |
| units | string or null |
| units_type | string or null |
| amount | string Decimal amount. |
| currency | string |
| wallet | string The wallet that was debited. |
object (Metadata) Up to 40 of your own key–value pairs, echoed back verbatim. | |
| created_at | string <date-time> |
{- "provider": "nawec_gm",
- "meter_number": "01234567890",
- "amount": "500.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}
}{- "id": "el_3Fk29s",
- "object": "electricity_token",
- "status": "processing",
- "provider": "nawec_gm",
- "meter_number": "01234567890",
- "token": "1234-5678-9012-3456-7890",
- "units": "38.4",
- "units_type": "kWh",
- "amount": "500.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}| limit | integer [ 1 .. 100 ] Default: 25 |
| starting_after | string Cursor — the |
| object | string Value: "list" |
| has_more | boolean |
Array of objects (ElectricityToken) |
{- "object": "list",
- "has_more": true,
- "data": [
- {
- "id": "el_3Fk29s",
- "object": "electricity_token",
- "status": "processing",
- "provider": "nawec_gm",
- "meter_number": "01234567890",
- "token": "1234-5678-9012-3456-7890",
- "units": "38.4",
- "units_type": "kWh",
- "amount": "500.00",
- "currency": "GMD",
- "wallet": "wal_3Km72dP",
- "metadata": {
- "order_id": "9912"
}, - "created_at": "2019-08-24T14:15:22Z"
}
]
}| limit | integer [ 1 .. 100 ] Default: 25 |
| starting_after | string Cursor — the |
| type | string Example: type=payout.paid Filter by event type. Supports a trailing wildcard ( |
{- "object": "list",
- "data": [
- {
- "id": "evt_2Kd93mA",
- "object": "event",
- "type": "payout.paid",
- "created_at": "2026-08-06T10:20:03Z",
- "data": {
- "object": {
- "id": "po_9Ks82mQz",
- "object": "payout",
- "status": "paid"
}
}
}
], - "has_more": true
}Manage webhook endpoints and their signing secrets. Register a URL and the event types you care about; PayKunda then POSTs a signed JSON event to your URL whenever the state of a payout, airtime top-up, data bundle or electricity token changes.
Every delivery includes a Webhook-Signature header:
Webhook-Signature: t=1754901120,v1=5257a869e7c3…
t — the UNIX timestamp when the event was signed.v1 — HMAC_SHA256(signing_secret, t + "." + raw_request_body), hex-encoded.The signing_secret (whsec_…) is returned once when you create the
endpoint. Store it securely — it is never shown again (rotate the endpoint
to get a new one).
To verify a delivery on your server:
t and v1 from the Webhook-Signature header.expected = HMAC_SHA256(signing_secret, t + "." + raw_body).expected to v1 using a constant-time comparison.|now - t| > 5 minutes (replay protection).// Node.js (Express raw body)
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(parts.t + "." + rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
return ok && fresh;
}
Return 2xx quickly to acknowledge receipt; PayKunda retries non-2xx
deliveries with exponential backoff. As a financial best practice, treat a
webhook as a notification and confirm the final state by fetching the
resource by id (e.g. GET /payouts/{payout_id}) before acting on it.
The signing secret (whsec_…) is returned once.
| url required | string |
| events required | Array of strings |
{- "events": [
- "payout.*",
- "electricity_token.delivered"
]
}{- "id": "we_4Rt29xP",
- "object": "webhook_endpoint",
- "events": [
- "payout.*",
- "electricity_token.delivered"
], - "secret": "whsec_8Kd02mYq71Zw",
- "status": "active",
- "created_at": "2026-08-06T10:12:00Z"
}{- "object": "list",
- "data": [
- {
- "id": "we_4Rt29xP",
- "object": "webhook_endpoint",
- "events": [
- "payout.*"
], - "status": "active"
}
], - "has_more": false
}