alpha-mintamir/chapa-agent-skills

chapa-payments

Integrate the Chapa payment gateway (Ethiopia) into any backend or frontend - hosted checkout, verify, webhooks with HMAC signature checks, direct charge (telebirr, M-Pesa, CBEBirr, ebirr), split payments, refunds, transfers, test cards and test numbers.

Quelltext ansehen
Originales Skill-Dokument

Aus dem Quell-Repository gerendert; Überschriften, Beispiele, Code, Tabellen, Links und Bilder bleiben erhalten.

Chapa Payments

Chapa is a payment gateway for Ethiopia (telebirr, CBEBirr, M-Pesa, Awash, ebirr, cards, PayPal). Base URL https://api.chapa.co/v1. Every server call is Authorization: Bearer <SECRET_KEY>. The instructions below come from the official docs at https://developer.chapa.co and were validated against a production integration. Follow them exactly; payment bugs cost real money.

Pick the integration

NeedUseReference
Web or mobile app, any payment method, fastest and safestStandard (hosted) checkout: server calls initialize, redirects to checkout_urlthis file
Static site, no backend for initializeHTML checkout form posting to /v1/hosted/payfrontend.md
Embedded form on your page (public key only)Inline.js ChapaCheckoutfrontend.md
Flutter appchapasdk package (native or web checkout)flutter.md
Own UI for USSD / OTP wallets, POS, ERPDirect Charge /v1/charges?type=... then /v1/validateadvanced.md
Marketplace payouts to vendorsSubaccounts + subaccounts in initialize, or Transfers APIadvanced.md

Default to Standard checkout unless the user asks otherwise. All of them end with the same rule: the server verifies with `GET /v1/transaction/verify/{tx_ref}` before giving value.

Standard checkout flow

1. Server  POST /v1/transaction/initialize  {amount, currency, tx_ref, callback_url, return_url, ...}
           -> data.checkout_url
2. Browser redirected to checkout_url, customer pays on Chapa's page
3. Chapa   GET  callback_url?trx_ref=..&ref_id=..&status=..    (server to server, may arrive before 4)
           302  return_url                                       (customer's browser)
           POST webhook URL                                      (if enabled in dashboard)
4. Server  GET /v1/transaction/verify/{tx_ref} on EVERY one of those hits
           accept only if data.status == "success" AND amount, currency, tx_ref match your record
5. Server  marks the order paid exactly once (idempotent), then shows the result page

Initialize payload

json
{
  "amount": "100.00",
  "currency": "ETB",
  "tx_ref": "order-1042-8f3k2m",
  "callback_url": "https://example.com/payments/chapa/callback?payment_id=1042",
  "return_url": "https://example.com/payments/chapa/return?payment_id=1042",
  "email": "abebe@example.com",
  "first_name": "Abebe",
  "last_name": "Bikila",
  "phone_number": "0912345678",
  "customization": { "title": "Lomi Store", "description": "Order 1042" },
  "meta": { "order_id": "1042" }
}

Rules that cause real failures when ignored:

  • amount, currency, tx_ref are required. currency is ETB or USD only (Direct Charge: ETB only).
  • tx_ref must be unique per attempt. Chapa rejects reuse with Transaction reference has been used before (400). Generate a fresh one for every retry and store it on your payment record. Use only letters, digits, - and _ (it becomes a URL path segment in verify).
  • phone_number is optional, but if sent it must be exactly 10 digits in 09xxxxxxxx or 07xxxxxxxx form. Convert +2519... / 2519... to 09... or omit the field.
  • email, first_name, last_name are optional. Only send an email that passes validation.
  • customization.title is short (16 characters is safe) and both title and description should be plain text (letters, numbers, spaces). Strip other symbols; Chapa validates these.
  • Send JSON with Content-Type: application/json. Wrong content type returns 400 Incorrect header settings.
  • Success looks like {"message":"Hosted Link","status":"success","data":{"checkout_url":"https://checkout.chapa.co/checkout/payment/..."}}. Treat anything else as failure and log message.
  • callback_url and return_url may be the same URL. Both are optional but you want both.
  • Optional meta keys: hide_receipt, disable_phone_edit, custom_receipt_enabled, invoices (array of {key, value}), payment_reason, plus any custom keys.

Verify

GET /v1/transaction/verify/{tx_ref} returns:

json
{ "message": "Payment details", "status": "success",
  "data": { "status": "success", "amount": 100, "currency": "ETB", "tx_ref": "order-1042-8f3k2m",
            "reference": "6jnheVKQEmy", "charge": 3.5, "mode": "test", "method": "telebirr",
            "type": "API", "first_name": "...", "email": "...", "meta": null, "created_at": "..." } }
  • data.status is one of success, pending, failed. Only success gives value.
  • A pending payment returns 404 Payment not paid yet. Do not mark it failed; the webhook will confirm later.
  • Compare data.amount (>= expected, tolerate 0.01), data.currency, data.tx_ref, and data.mode against your record.
  • Key/mode mismatch returns 401: Live secret keys can't be used to verify a test transaction (and the reverse). Use the key of the mode the payment was created in.
  • Never trust status from the callback query string or the return URL. Only trust verify.

Callback vs return vs webhook

  • Callback: GET callback_url with trx_ref, ref_id, status in the query. Server to server, fires once payment completes, may race the browser redirect.
  • Return: the customer's browser lands on return_url. Run the same verify logic, then render success or "still processing".
  • Webhook: POST JSON to the URL configured in Dashboard > Settings > Webhooks. Required for mobile money that completes after the customer leaves, and for abandoned tabs. See webhooks.md.
  • Make the "mark paid" step atomic (UPDATE ... WHERE id = ? AND is_paid = 0) and run the success side effects only when that update changed a row. All three channels can hit at the same second.

Webhook essentials

  • Body is JSON with event (charge.success, charge.failed/cancelled, charge.refunded, charge.reversed, payout.success, payout.failed/cancelled), tx_ref, reference, amount, currency, status, payment_method, type, mode, meta.
  • Headers: x-chapa-signature = HMAC SHA256 of the payload signed with your webhook secret hash; chapa-signature = HMAC SHA256 of the secret hash signed with itself. Either matching header is sufficient; missing or mismatched means reject.
  • Compute the payload HMAC over the raw request body. Chapa's own sample uses JSON.stringify(req.body), so also accept the HMAC of the re-serialized body as a fallback.
  • Disable CSRF on the webhook route. Return 200 fast. Without a 200 Chapa retries every 10 minutes, up to 10 times over 72 hours, so the handler must be idempotent.
  • After the signature check, still call verify before giving value (Chapa says so, and it protects against replay with a forged body).
  • The secret hash is any random string you choose; it is not the API key. Store it as an env var and paste the same value in the dashboard.

Configuration

Environment variable names to use unless the project already has a convention:

CHAPA_PUBLIC_KEY=CHAPUBK_TEST-...     # CHAPUBK-... in live
CHAPA_SECRET_KEY=CHASECK_TEST-...     # CHASECK-... in live
CHAPA_WEBHOOK_SECRET=<random string you also paste in Dashboard > Settings > Webhooks>
CHAPA_ENCRYPTION_KEY=...              # only for Direct Charge OTP/card payloads (Settings > API)
  • Test keys carry _TEST in the prefix. Test mode still sends webhooks and emails but only test cards and test numbers succeed.
  • Keys live in Dashboard > Settings > API Keys. The dashboard toggle switches which set is shown.
  • Never ship the secret key to a browser or mobile app. Public key only on the client (HTML checkout, Inline.js, Flutter).
  • When building an admin config screen (multi vendor platforms, CMS plugins), store public_key, secret_key, webhook_secret_hash per mode (test / live), show the callback and webhook URLs read-only so the merchant can paste them into Chapa, and list ETB and USD as the only supported currencies.

Implementation checklist

Copy and track:

- [ ] Env vars / admin settings for public key, secret key, webhook secret (test and live)
- [ ] Payment record table has: tx_ref (unique), amount, currency, status, chapa_reference
- [ ] Initialize endpoint: fresh tx_ref per attempt, phone normalised, payload validated, redirect to checkout_url
- [ ] Callback + return handler: verify, compare amount/currency/tx_ref, atomic mark paid, pending stays pending
- [ ] Webhook POST handler: CSRF off, signature check (both headers), verify, idempotent, 200 response
- [ ] Failure path: log Chapa `message`, show retry option, never mark paid
- [ ] Test in test mode with test card 4200 0000 0000 0000 or test number 0900123456, then switch keys to live
- [ ] Confirm webhook URL is HTTPS and publicly reachable (use a tunnel locally)

Testing

  • Cards: Visa 4200 0000 0000 0000, Mastercard 5400 0000 0000 0000, UnionPay 6200 0000 0000 0000, Diners 3800 0000 0000 0000 (CVV 123), Amex 3700 0000 0000 0000 (CVV 1234). Expiry 12/34.
  • Mobile money success numbers: 0900123456, 0900112233, 0900881111 (Awash and Amole OTP 12345); M-Pesa 0700123456, 0700112233, 0700881111. Any other number fails in test mode.
  • scripts/init_test_payment.sh creates a checkout link; scripts/verify_transaction.sh <tx_ref> prints the verify response; scripts/sign_webhook.py computes both signature headers and can POST a signed sample event to your local endpoint.

Stack guides

Read only the one that matches the project:

  • laravel.md: controller with initialize, callback/return, webhook, plus the official chapa/chapa-laravel package.
  • nodejs.md: Express with raw body capture, Next.js App Router route handlers, NestJS pointer, chapa-nodejs SDK.
  • python.md: Django views and FastAPI routes, chapa package.
  • php.md: plain PHP with cURL for WordPress plugins, legacy apps and cPanel hosting.
  • frontend.md: HTML checkout, Inline.js, React usage, and why the frontend must never be the source of truth.
  • flutter.md: chapasdk native and web checkout with server side verify.
  • advanced.md: Direct Charge (USSD / OTP / portal), encryption, split payments and subaccounts, refunds, transfers, balance, banks, cancel.
  • api-reference.md: every endpoint, field, response and error message in one place.
  • webhooks.md: payload shapes for each event and signature verification in PHP, Node, Python, Go.

Common mistakes to catch in review

  • Marking paid from the status=success query parameter without calling verify.
  • Reusing tx_ref on retry, then reporting "Chapa is down" when it is a 400.
  • Sending +251912345678 as phone_number.
  • Webhook route behind CSRF or auth middleware, so Chapa gets 419/401 and retries for 72 hours.
  • Verifying a test payment with the live key after switching the dashboard toggle.
  • Treating pending as failed and cancelling an order that completes 30 seconds later.
  • Comparing amounts as floats without rounding, or forgetting the currency check when both ETB and USD are enabled.
  • Storing only one set of keys when the platform lets merchants toggle test and live.