Webhooks
Instead of polling status endpoints, Link App POSTs to your server when a payment or payout finishes. This is the recommended approach for production.
Register your webhook URL with Chapa (via your account manager or dashboard).
Events
| Event | Meaning |
|---|---|
payment.success | Card was topped up |
payment.failed | Payment failed |
payment.cancelled | Payment was cancelled |
payout.success | Payout completed |
payout.failed | Payout failed |
Payment vs payout payloads
Payment and payout webhooks use different field sets. Do not expect payment fields on payout events (and vice versa).
| Payment webhooks | Payout webhooks | |
|---|---|---|
| Primary reference | link_app_reference | initiator_reference or chapa_reference |
| Type field | payment_type | payout_type |
| Extra context | processor_reference, payment_method | meta (destination details) |
| Not included | initiator_reference, chapa_reference, payout_type | link_app_reference, processor_reference, payment_type, payment_method |
Optional fields are omitted when empty — you will not receive null placeholders.
Field order
Payload keys are sent in a stable, logical order:
event,status,mode- Type (
payment_typeorpayout_type),currency,amount - References and context (
customer,meta, …) created_at,updated_atlast
Shared fields
| Field | Description |
|---|---|
event | What happened (payment.success, payout.failed, …) |
status | success, failed, or cancelled |
mode | live or test |
currency | e.g. ETB |
amount | Amount in the card currency (number) |
service_fee | Fee on the transaction, when applicable (payments) |
merchant_reference | Your order ID, if you sent one |
customer | Payer or cardholder (first_name, last_name, email, phone_number) |
created_at | ISO 8601 timestamp |
updated_at | ISO 8601 timestamp |
Payment webhooks
| Field | Description |
|---|---|
link_app_reference | Same as link_reference from the payment API |
processor_reference | Chapa payment reference, when available |
payment_type | e.g. link, donation, event, qr_code, verification |
payment_method | e.g. telebirr |
{
"event": "payment.success",
"status": "success",
"mode": "live",
"currency": "ETB",
"amount": 100,
"merchant_reference": "ORDER-2026-001",
"link_app_reference": "CARD-R79130925F",
"processor_reference": "CHr7eQE07ZFB",
"payment_type": "link",
"payment_method": "telebirr",
"customer": {
"first_name": "Abebe",
"last_name": "Bikila",
"phone_number": "960724272"
},
"created_at": "2026-06-21T12:00:00.000000Z",
"updated_at": "2026-06-21T12:00:00.000000Z"
}Payout webhooks
| Field | Description |
|---|---|
payout_type | card_to_card, bank, or b2b |
initiator_reference | Card-to-card correlation ID — same value returned from POST /card/payouts |
chapa_reference | Bank or B2B correlation ID — same value returned from POST /card/payouts |
meta | Destination details only; never repeats top-level references or payout_type |
Each payout webhook includes one primary reference:
- Card-to-card →
initiator_reference - Bank or B2B →
chapa_reference
payout_type values
payout_type | Reference field | meta fields |
|---|---|---|
card_to_card | initiator_reference | source_card_number, destination_card_number |
bank | chapa_reference | source_card_number, bank_slug, account_number, account_name |
b2b | chapa_reference | source_card_number, merchant_id |
Bank payouts are finalized asynchronously when Chapa sends a payout status update. B2B payouts are finalized in the same API response when Chapa returns success.
Card-to-card
{
"event": "payout.success",
"status": "success",
"mode": "live",
"payout_type": "card_to_card",
"currency": "ETB",
"amount": 100,
"initiator_reference": "GCy79131114X",
"customer": {
"first_name": "Abebe",
"last_name": "Bikila",
"phone_number": "960724272"
},
"meta": {
"source_card_number": "7878064900",
"destination_card_number": "8787064900"
},
"created_at": "2026-06-21T12:00:00.000000Z",
"updated_at": "2026-06-21T12:00:00.000000Z"
}Bank withdrawal
{
"event": "payout.success",
"status": "success",
"mode": "live",
"payout_type": "bank",
"currency": "ETB",
"amount": 100,
"chapa_reference": "CP17815960544446",
"customer": {
"first_name": "Abebe",
"last_name": "Bikila",
"phone_number": "960724272"
},
"meta": {
"source_card_number": "5610764625",
"bank_slug": "telebirr",
"account_number": "0926760003",
"account_name": "Abebe Bikila"
},
"created_at": "2026-06-21T12:00:00.000000Z",
"updated_at": "2026-06-21T12:00:00.000000Z"
}B2B merchant payout
{
"event": "payout.success",
"status": "success",
"mode": "live",
"payout_type": "b2b",
"currency": "ETB",
"amount": 100,
"chapa_reference": "CP17815960544446",
"customer": {
"first_name": "Abebe",
"last_name": "Bikila",
"phone_number": "960724272"
},
"meta": {
"source_card_number": "5610764625",
"merchant_id": "MERCH-12345"
},
"created_at": "2026-06-21T12:00:00.000000Z",
"updated_at": "2026-06-21T12:00:00.000000Z"
}Verifying webhooks
Chapa signs every webhook. Each request includes a link-app-signature header. Verify it with the webhook secret Chapa gives you when you register your URL.
Always verify the signature before updating orders or balances on your side.
Verification steps:
- Read
link-app-signaturefrom the request header. - Load your
WEBHOOK_SECRETfrom secure storage. - Compute
HMAC-SHA256(WEBHOOK_SECRET, WEBHOOK_SECRET)(sign the secret with itself). - Compare the result to the header value. If they match, the request is authentic.
Setup
- Create an HTTPS endpoint on your server (e.g.
POST /webhooks/link-app). - Register the URL with Chapa.
- Return
200quickly — do heavy work in the background. - Handle duplicate deliveries safely (retries can happen).
- Branch on
eventand, for payouts, onpayout_type.
For dashboards and reconciliation, you can also fetch history with GET /card/payments and GET /card/payouts instead of relying only on webhooks.