ChapaChapa Docs

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.

FieldTypeDescription
webhook_typestringCategory: payment, payout, refund
eventstringEvent name (e.g., payment.success)
statusstringCurrent state of the transaction
modestringlive or test
currencystringISO currency code (ETB, USD, UGX, DJF)
amountstringAmount involved in the event
merchant_referencestringMerchant-generated reference
chapa_referencestringChapa-generated reference
created_atstringISO8601 creation timestamp
updated_atstringISO8601 update timestamp
metaobjectMerchant-defined metadata

Payment Webhook Responses

Payment-Specific Fields

FieldTypeDescription
payment_typestringUse-case (API, Event, Donation, Direct charge, Hosted, etc.)
payment_methodstringMethod used (telebirr, card, bank_transfer, etc.)
service_feestringFee charged by Chapa (if applicable)
customerobjectPayer information
reasonstringAppears on failure/incomplete/blocked
cancelled_bystringAppears on cancelled (USER or SYSTEM)
refunded_amountstringAppears on refund events
auth_typestringAppears on auth-needed
expires_atstringAppears on auth-needed

Customer Object

FieldTypeDescription
first_namestringCustomer first name
last_namestringCustomer last name
emailstringCustomer email
phone_numberstringCustomer 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

FieldTypeDescription
payout_typestringcustomer_payout, merchant_payout, bulk_payout
service_feestringFee charged for processing
processor_referencestringReference from bank/provider
accountobjectDestination account info
reasonstringPresent on failures/blocks/reversals
auth_typestringPresent on auth-needed
otp_channelarrayPresent on otp-needed
otp_attemptsnumberPresent on otp-failed

Account Object

FieldTypeDescription
account_namestringName on receiving account
account_numberstringDestination account number
bank_slugstringProvider identifier
bank_namestringProvider 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

TypeIdentifiersExtra FieldsTypical Events
Paymentmerchant_reference, chapa_referencepayment_type, payment_method, customer, service_feepayment.success, payment.failed, payment.cancelled
Payoutmerchant_reference, chapa_reference, processor_referencepayout_type, account, otp_channel, auth_typepayout.success, payout.failed, payout.reversed
RefundSent as payment lifecycle eventsrefunded_amountpayment.partially_refunded, payment.fully_refunded

Event Handling Patterns

1) Idempotency (Must Have)

Chapa may deliver the same webhook more than once.

Recommended strategy:

  1. Create a webhook_events table
  2. Store a deduplication key such as: event + chapa_reference + status + updated_at
  3. Then:
    • If already processed → return 200 OK immediately
    • Otherwise → process and store

2) Retries

If your endpoint fails (non-200 or timeout), Chapa may retry.

Your endpoint should:

  • Return 200 OK quickly
  • 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

On this page