Mode d’emploi
- Copiez la commande et examinez le paquet avant de l’installer dans votre environnement agent.
- Consultez le document original pour les outils, identifiants et environnements requis.
- Décrivez les entrées et le livrable souhaité ; comparez la sortie aux sources.
npx skills add https://github.com/alpha-mintamir/chapa-agent-skills --skill chapa-paymentsAvant de commencer
- L’installation n’inclut ni abonnement au modèle, ni quota API, ni accès aux fichiers.
- Étoiles et installations reflètent l’activité, pas un test indépendant de qualité ou de sécurité.
Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
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
| Need | Use | Reference |
|---|---|---|
| Web or mobile app, any payment method, fastest and safest | Standard (hosted) checkout: server calls initialize, redirects to checkout_url | this file |
| Static site, no backend for initialize | HTML checkout form posting to /v1/hosted/pay | frontend.md |
| Embedded form on your page (public key only) | Inline.js ChapaCheckout | frontend.md |
| Flutter app | chapasdk package (native or web checkout) | flutter.md |
| Own UI for USSD / OTP wallets, POS, ERP | Direct Charge /v1/charges?type=... then /v1/validate | advanced.md |
| Marketplace payouts to vendors | Subaccounts + subaccounts in initialize, or Transfers API | advanced.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 pageInitialize payload
{
"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_refare required.currencyisETBorUSDonly (Direct Charge:ETBonly).tx_refmust be unique per attempt. Chapa rejects reuse withTransaction 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_numberis optional, but if sent it must be exactly 10 digits in09xxxxxxxxor07xxxxxxxxform. Convert+2519.../2519...to09...or omit the field.email,first_name,last_nameare optional. Only send an email that passes validation.customization.titleis 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 400Incorrect 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 logmessage. callback_urlandreturn_urlmay be the same URL. Both are optional but you want both.- Optional
metakeys: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:
{ "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.statusis one ofsuccess,pending,failed. Onlysuccessgives 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, anddata.modeagainst 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
statusfrom the callback query string or the return URL. Only trust verify.
Callback vs return vs webhook
- Callback:
GET callback_urlwithtrx_ref,ref_id,statusin 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:
POSTJSON 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
200fast. 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
_TESTin 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_hashper mode (test / live), show the callback and webhook URLs read-only so the merchant can paste them into Chapa, and listETBandUSDas 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, Mastercard5400 0000 0000 0000, UnionPay6200 0000 0000 0000, Diners3800 0000 0000 0000(CVV123), Amex3700 0000 0000 0000(CVV1234). Expiry12/34. - Mobile money success numbers:
0900123456,0900112233,0900881111(Awash and Amole OTP12345); M-Pesa0700123456,0700112233,0700881111. Any other number fails in test mode. scripts/init_test_payment.shcreates a checkout link;scripts/verify_transaction.sh <tx_ref>prints the verify response;scripts/sign_webhook.pycomputes 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-laravelpackage. - nodejs.md: Express with raw body capture, Next.js App Router route handlers, NestJS pointer,
chapa-nodejsSDK. - python.md: Django views and FastAPI routes,
chapapackage. - 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:
chapasdknative 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=successquery parameter without calling verify. - Reusing
tx_refon retry, then reporting "Chapa is down" when it is a 400. - Sending
+251912345678asphone_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
pendingas 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.



