Webhooks
Receive real-time notifications when payments, payouts, and refunds occur.
Webhooks allow Chapa to notify your system in real time when important events occur, such as:
- Payment success or failure
- Payment cancellation or timeout
- Refunds (partial or full)
- Payout status updates
Instead of polling the API, Chapa pushes event data to your server as soon as the transaction state changes.
Webhooks are the most reliable way to track payments, payouts, and refunds.
Chapa sends webhooks to notify merchants about payments, payouts, and refunds. All webhook payloads follow a consistent base structure, with additional fields depending on:
webhook_type(payment, payout, refund)event(e.g., payment.success, payout.failed)status(e.g., success, failed, blocked)- The specific business flow (hosted, direct charge, bulk payout, etc.)
Use webhooks for real-time updates, but process them idempotently and safely.
Common Payload Structure (All Webhooks)
These fields appear in all webhook payloads, regardless of webhook type.
| Field | Type | Description |
|---|---|---|
webhook_type | string | Category: payment, payout, refund |
event | string | Event name (e.g., payment.success) |
status | string | Current state of the transaction |
mode | string | live or test |
currency | string | ISO currency code (ETB, USD, UGX, DJF) |
amount | string | Amount involved in the event |
merchant_reference | string | Merchant-generated reference |
chapa_reference | string | Chapa-generated reference |
created_at | string | ISO8601 creation timestamp |
updated_at | string | ISO8601 update timestamp |
meta | object | Merchant-defined metadata |
Payment Webhook Responses
Payment-Specific Fields
| Field | Type | Description |
|---|---|---|
payment_type | string | Use-case (API, Event, Donation, Direct charge, Hosted, etc.) |
payment_method | string | Method used (telebirr, card, bank_transfer, etc.) |
service_fee | string | Fee charged by Chapa (if applicable) |
customer | object | Payer information |
reason | string | Appears on failure/incomplete/blocked |
cancelled_by | string | Appears on cancelled (USER or SYSTEM) |
refunded_amount | string | Appears on refund events |
auth_type | string | Appears on auth-needed |
expires_at | string | Appears on auth-needed |
Customer Object
| Field | Type | Description |
|---|---|---|
first_name | string | Customer first name |
last_name | string | Customer last name |
email | string | Customer email |
phone_number | string | Customer phone (international format) |
1) SUCCESS — payment.success
{
"webhook_type": "payment",
"event": "payment.success",
"status": "success",
"mode": "live",
"payment_type": "Event",
"currency": "ETB",
"amount": "40000",
"service_fee": "1200",
"merchant_reference": "TXN123SUCCESS",
"chapa_reference": "CHREF123",
"payment_method": "telebirr",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}2) FAILED — payment.failed
Additional field: reason (example: INSUFFICIENT_FUNDS)
{
"webhook_type": "payment",
"event": "payment.failed",
"status": "failed",
"mode": "live",
"payment_type": "API",
"currency": "ETB",
"amount": "40000",
"service_fee": "1200",
"merchant_reference": "TXN123FAILED",
"chapa_reference": "CHREF123",
"reason": "INSUFFICIENT_FUNDS",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}3) CANCELLED — payment.cancelled
Additional field: cancelled_by (USER or SYSTEM)
{
"webhook_type": "payment",
"event": "payment.cancelled",
"status": "cancelled",
"mode": "live",
"payment_type": "Donation",
"currency": "ETB",
"amount": "40000",
"service_fee": "1200",
"merchant_reference": "TXN123CANCELLED",
"chapa_reference": "CHREF123",
"cancelled_by": "USER",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}4) INCOMPLETE — payment.incomplete
Additional field: reason (example: TIMEOUT, ABANDONED)
{
"webhook_type": "payment",
"event": "payment.incomplete",
"status": "incomplete",
"mode": "live",
"payment_type": "API",
"currency": "ETB",
"amount": "40000",
"service_fee": "1200",
"merchant_reference": "TXN123INCOMPLETE",
"chapa_reference": "CHREF123",
"reason": "TIMEOUT",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}5) PARTIALLY_REFUNDED — payment.partially_refunded
Additional field: refunded_amount
{
"webhook_type": "refund",
"event": "payment.partially_refunded",
"status": "partially_refunded",
"mode": "live",
"payment_type": "API",
"currency": "ETB",
"amount": "40000",
"refunded_amount": "15000",
"service_fee": "0",
"merchant_reference": "TXN123PARTIALREFUND",
"chapa_reference": "CHREF123",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}6) FULLY_REFUNDED — payment.fully_refunded
Additional field: refunded_amount (equals original amount)
{
"webhook_type": "refund",
"event": "payment.fully_refunded",
"status": "fully_refunded",
"mode": "live",
"payment_type": "Event",
"currency": "ETB",
"amount": "40000",
"refunded_amount": "40000",
"service_fee": "0",
"merchant_reference": "TXN123FULLREFUND",
"chapa_reference": "CHREF123",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}7) AUTH_NEEDED — payment.auth_needed
Additional fields: auth_type, expires_at
{
"webhook_type": "payment",
"event": "payment.auth_needed",
"status": "auth_needed",
"mode": "live",
"payment_type": "Direct charge",
"currency": "ETB",
"amount": "40000",
"service_fee": "1200",
"merchant_reference": "TXN123AUTH",
"chapa_reference": "CHREF123",
"auth_type": "PIN",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T12:00:00Z",
"updated_at": "2025-11-07T13:00:00Z",
"expires_at": "2025-11-07T13:00:00Z"
}8) BLOCKED — payment.blocked
Additional field: reason (example: COMPLIANCE_REVIEW)
{
"webhook_type": "payment",
"event": "payment.blocked",
"status": "blocked",
"mode": "live",
"payment_type": "API",
"currency": "ETB",
"amount": "40000",
"service_fee": "1200",
"merchant_reference": "TXN123BLOCKED",
"chapa_reference": "CHREF123",
"reason": "COMPLIANCE_REVIEW",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "251722927727"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}Payout Webhook Responses
Payout-Specific Fields
| Field | Type | Description |
|---|---|---|
payout_type | string | customer_payout, merchant_payout, bulk_payout |
service_fee | string | Fee charged for processing |
processor_reference | string | Reference from bank/provider |
account | object | Destination account info |
reason | string | Present on failures/blocks/reversals |
auth_type | string | Present on auth-needed |
otp_channel | array | Present on otp-needed |
otp_attempts | number | Present on otp-failed |
Account Object
| Field | Type | Description |
|---|---|---|
account_name | string | Name on receiving account |
account_number | string | Destination account number |
bank_slug | string | Provider identifier |
bank_name | string | Provider display name |
1) SUCCESS — payout.success
{
"webhook_type": "payout",
"event": "payout.success",
"status": "success",
"payout_type": "customer_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123SUCCESS",
"chapa_reference": "CHP123SUCCESS",
"processor_reference": "BANKREF123",
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}2) FAILED — payout.failed
Additional field: reason (example: BANK_REJECTED)
{
"webhook_type": "payout",
"event": "payout.failed",
"status": "failed",
"payout_type": "customer_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123FAILED",
"chapa_reference": "CHP123FAILED",
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"reason": "BANK_REJECTED",
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}3) REVERSED — payout.reversed
Additional field: reason (example: BENEFICIARY_ACCOUNT_ISSUE)
{
"webhook_type": "payout",
"event": "payout.reversed",
"status": "reversed",
"payout_type": "customer_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123REVERSED",
"chapa_reference": "CHP123REVERSED",
"processor_reference": "BANKREF123",
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"reason": "BENEFICIARY_ACCOUNT_ISSUE",
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}4) BLOCKED — payout.blocked
Additional field: reason (example: COMPLIANCE_REVIEW)
{
"webhook_type": "payout",
"event": "payout.blocked",
"status": "blocked",
"payout_type": "customer_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123BLOCKED",
"chapa_reference": "CHP123BLOCKED",
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"reason": "COMPLIANCE_REVIEW",
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}5) AUTH_NEEDED — payout.auth_needed
Additional field: auth_type
{
"webhook_type": "payout",
"event": "payout.auth_needed",
"status": "auth_needed",
"payout_type": "customer_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123AUTH",
"chapa_reference": "CHP123AUTH",
"auth_type": "PIN",
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}6) OTP_NEEDED — payout.otp_needed
Additional field: otp_channel (example: ["sms", "email", "telegram"])
{
"webhook_type": "payout",
"event": "payout.otp_needed",
"status": "otp_needed",
"payout_type": "customer_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123OTP",
"chapa_reference": "CHP123OTP",
"otp_channel": ["sms", "email", "telegram"],
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}7) OTP_FAILED — payout.otp_failed
Additional field: otp_attempts
{
"webhook_type": "payout",
"event": "payout.otp_failed",
"status": "otp_failed",
"payout_type": "merchant_payout",
"currency": "ETB",
"amount": "200000",
"service_fee": "6000",
"merchant_reference": "PAYOUT123OTPFAILED",
"chapa_reference": "CHP123OTPFAILED",
"account": {
"account_name": "Customer Name",
"account_number": "123567890987",
"bank_slug": "cbe",
"bank_name": "Commercial Bank of Ethiopia"
},
"otp_attempts": 3,
"meta": {
"order_id": "ORD-99821"
},
"created_at": "2025-11-07T13:00:00Z",
"updated_at": "2025-11-07T13:00:00Z"
}Payment vs Payout vs Refund Payload Differences
| Type | Identifiers | Extra Fields | Typical Events |
|---|---|---|---|
| Payment | merchant_reference, chapa_reference | payment_type, payment_method, customer, service_fee | payment.success, payment.failed, payment.cancelled |
| Payout | merchant_reference, chapa_reference, processor_reference | payout_type, account, otp_channel, auth_type | payout.success, payout.failed, payout.reversed |
| Refund | Sent as payment lifecycle events | refunded_amount | payment.partially_refunded, payment.fully_refunded |
Event Handling Patterns
1) Idempotency (Must Have)
Chapa may deliver the same webhook more than once.
Recommended strategy:
- Create a
webhook_eventstable - Store a deduplication key such as:
event + chapa_reference + status + updated_at - Then:
- If already processed → return
200 OKimmediately - Otherwise → process and store
- If already processed → return
2) Retries
If your endpoint fails (non-200 or timeout), Chapa may retry.
Your endpoint should:
- Return
200 OKquickly - Process heavy logic asynchronously (queue/job) if possible
- Be safe for repeats (idempotent)
3) Ordering
Webhooks can arrive:
- Out of order
- Close together
- With state transitions (
pending → success)
Recommended:
- Update transaction state only if the new state is newer (
updated_at) - Or enforce state progression rules (
pending → success/failed)
4) Verification Strategy
Webhooks are the best real-time signal. Verification is the best confirmation.
Recommended approach:
- Use webhook to update state immediately
- Verify if:
- Order is high-value
- State is unusual (
blocked/auth_needed) - Your fulfillment depends on strict confirmation
Next Steps
- Verify Payments - Confirm payment status server-side
- Accept Payments - Full hosted payment integration
- Quick Start - End-to-end payment flow