# Web Rabbit Payments API Documentation # Introduction > What Web Rabbit Payments is and how the API is structured. The Web Rabbit Payments API lets you accept Mobile Money across Africa (plus bank transfer in Nigeria) with a single integration. Live route health is on the [status page](https://webrabbitmedia.com/docs/system-status). You get one API, one dashboard, and one settlement — we handle the rails. ## Overview The API is organised around REST. It uses predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes and verbs. ## Base URL All endpoints are served over HTTPS at: `https://api.webrabbitmedia.com/v1` > **One base URL — mode is inferred from the key** There is no separate host to call. The mode of the request ( test or live ) is determined by the key prefix: `wr_test_...` runs against our built-in simulator, `wr_live_...` hits production rails. Test and live data are fully isolated. > **Machine-readable contract** The whole API — Payments, Messaging, Bills and every webhook event — is published as an OpenAPI 3.1 document at `https://api.webrabbitmedia.com/openapi.json` (and `https://webrabbitmedia.com/openapi.json`). Generate a client from it, validate payloads against it, and diff it between releases. Every page here is also served as plain Markdown at `/docs/.md`. See [OpenAPI & SDKs](https://webrabbitmedia.com/docs/openapi) and the [API changelog](https://webrabbitmedia.com/docs/changelog). ## Health check `GET https://api.webrabbitmedia.com/v1/health` is unauthenticated and returns `{ "ok": true, "service": "webrabbit-api", "request_id": "…" }` with HTTP `200`. Use it for uptime monitoring — it is still subject to the unauthenticated per-IP rate limit. ## Payment providers Behind the API we settle through licensed payment providers. OGateway is the default for payments and payouts in all 16 live countries; 360Pay (LibertePay) and JuniPay remain available in Ghana when we route a business to them. Each business is routed to one provider; the routing is managed by us and never changes your request or response shape. The normalised `status` (`approved` · `pending` · `failed`) is identical either way — only the raw `code` and `reason` passed through from upstream differ. See [Provider codes](https://webrabbitmedia.com/docs/provider-codes). > **Payouts are not part of the public API** `POST /v1/payout/momo` and `/v1/payout/bank` are retired and return `501 provider_unsupported`. Withdrawals are requested from Payouts in the merchant dashboard and settled by our operations team. ## Test mode & Live mode Every business has two independent environments. Live mode unlocks after your business is approved and processes real payments. > **Test mode is fully simulated** `wr_test_` keys are served by our built-in simulator: no upstream provider call, no real debit, amounts capped at GHS 10, and deterministic outcomes chosen by the amount. Read `test_moves_real_money` on `GET /v1/me` if you want to assert it. See [Test data](https://webrabbitmedia.com/docs/test-data). > **Live mode requires approval** You can create live API keys, but charges will fail until your business status is `approved`. Complete verification from the [Verification](https://webrabbitmedia.com/merchant/verification) page. Only GHS is supported today. Amounts are decimal (e.g. `10.50`) — we handle any pesewa-string conversion required by upstream rails. Ready to charge your first customer? [Jump to the Quickstart →](https://webrabbitmedia.com/docs/quickstart) --- # Quickstart > Create a key, charge a customer, and inspect a transaction in five minutes. You'll create a test API key, charge a Mobile Money number, and inspect the resulting transaction — end-to-end in about five minutes. ## Step 1 — Create a key Head to [Developer → API Keys](https://webrabbitmedia.com/merchant/developer/api-keys), click Create key , and copy the secret. You'll only see it once. > **Key format** Keys look like `wr_test_...` in test mode and `wr_live_...` once your business is approved. ## Step 2 — Make your first charge Send a `POST` to `/collect/momo`. `amount` is decimal GHS (e.g. `1.00`) — we handle any pesewa-string conversion required by the upstream provider. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/collect/momo \ -H "Authorization: Bearer wr_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1024" \ -H "HTTP-Referer: https://your-app-site.com" \ -H "X-Webrabbitmedia-Title: Your App Name" \ -d '{ "amount": 1.00, "subscriber_number": "0240000000", "network": "MTN", "desc": "Order 1024" }' ``` ## Step 3 — Inspect the transaction Every successful charge is written to your ledger. Open [Transactions → Payments](https://webrabbitmedia.com/merchant/transactions/payments) to see the gross amount, the platform fee, and your net settlement — or fetch it via the API. --- # Authentication > Authenticate every request with your secret API key. The Web Rabbit API uses bearer token authentication. Every request must include your secret key in the `Authorization` header. ## API keys Manage keys from Developer → API Keys . Keys are prefixed by mode (`wr_test_...` or `wr_live_...`) and scoped by access ( read or read + write , chosen at creation). Test keys work any time; live keys only work once your business is approved. Write scope is required for `/v1/checkout/session`. > **Keep secrets server-side** Never embed a secret key in a mobile app, browser bundle, or public repo. Anyone with your key can create charges on your behalf. > **One base URL — mode inferred from the key** There is no separate host to call. `wr_test_...` keys are answered by Web Rabbit's built-in simulator — no provider call, no prompt to a handset, no debit, amounts capped at GHS 10 — and `wr_live_...` keys hit production rails. Both go to `https://api.webrabbitmedia.com`; test and live data are fully isolated. See [Test data](https://webrabbitmedia.com/docs/test-data). ## Sending the key Authorization header ``` Authorization: Bearer wr_test_1a2b3c4d5e6f... ``` shell ``` curl https://api.webrabbitmedia.com/v1/me \ -H "Authorization: Bearer wr_test_1a2b3c4d5e6f..." ``` Requests without a valid key return `401 Unauthorized`. ## Identify your app Send two extra headers with every API request so Web Rabbit knows which site and product your key is powering. This is how we attribute traffic, spot key leaks, and review integrations. Attribution headers ``` HTTP-Referer: https://your-app-site.com X-Webrabbitmedia-Title: Your App Name ``` shell ``` curl https://api.webrabbitmedia.com/v1/collect/momo \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -H "HTTP-Referer: https://your-app-site.com" \ -H "X-Webrabbitmedia-Title: Your App Name" \ -d '{"amount":10,"phone":"0248000000","network":"mtn"}' ``` - HTTP-Referer — the public URL of the site or app using the key. If you omit it we fall back to the standard Referer or Origin header when present. - X-Webrabbitmedia-Title — the human-readable name of your product, shown to our review team. Every distinct site + app name appears under Developer → Applications in your dashboard, with request counts and review status. Requests that arrive without attribution still succeed, but the response carries a `x-wr-attribution-warning` header. > **Already integrated before these headers existed?** Nothing breaks. Existing keys keep working exactly as before — there is no cutover date and undeclared traffic is never blocked. It is simply grouped in your dashboard as Not declared with a request count, so you can see which integration still needs the headers. Add them when you next deploy. Server-to-server code sends no browser referer, so set `HTTP-Referer` explicitly to your site rather than relying on the fallback. > **Declarations are reviewed by humans** Attribution must be truthful. If a review finds the declared site or app name is false or does not match the live integration, the key — and in serious cases the account — can be suspended. If you move to a new domain or rebrand, tell us at support@webrabbitmedia.com before you switch so we can update the record. Live requests from a host we have blocked return `403 domain_blocked`; test mode is never blocked. ## Scopes — read vs read + write Each key is granted one of two scopes at creation, controlled by the Enable write access checkbox in the dashboard: - read (checkbox off) — can retrieve transactions and account info. - read + write (checkbox on) — everything read can do, plus create collections and checkout sessions. Scope required per endpoint: | Endpoint | Required scope | | --- | --- | | GET /v1/me | read | | GET /v1/transactions | read | | GET /v1/transactions/{id} | read | | POST /v1/collect/momo | write | | POST /v1/checkout/session | write | A request that requires `write` but is made with a read-only key returns `403 Forbidden` with this body: 403 insufficient_scope ``` { "error": "insufficient_scope", "required": "write", "granted": "read" } ``` The scope is baked into the key at creation and cannot be changed later — revoke and create a new key to change access. ## Preflight — check mode & approval Before you charge a real customer, call `GET /v1/me` to confirm the key's mode, scopes, and that the business is approved for live rails. See [Me](https://webrabbitmedia.com/docs/me). ## Legacy keys without a prefix Keys issued before prefixed-key rollout have no `wr_test_` / `wr_live_` prefix and continue to authenticate as-is — no rotation required. If you'd like a prefixed replacement, create a new key from Developer → API Keys and revoke the old one when you cut over. ## Rotating keys If a key is exposed, revoke it from the dashboard and create a new one. Rotation is instant — the revoked key stops working immediately. ## Keys created by teammates A key only works while the person who created it is still the business owner or an owner, admin or editor on the team. Removing a teammate, or lowering them to viewer, disables every key they created straight away (requests return `401 API key revoked`). For long-running integrations, have the business owner create the key. --- # Idempotency > Retry money-moving requests safely with an Idempotency-Key header. Network failures happen. Retrying a request without safeguards can double-charge a customer or send a charge twice. The `Idempotency-Key` header lets you retry safely. ## How it works Send a unique `Idempotency-Key` header (max 255 chars — a UUID is ideal) with any money-moving request. We store the response for 24 hours keyed by `(business, endpoint, key)`. Retries with the same key return the original response with an `Idempotent-Replayed: true` header. > **Supported endpoints** `POST /v1/collect/momo`. Hosted checkout sessions and read endpoints don't need it (reads are already idempotent). ## Example shell ``` curl -X POST https://api.webrabbitmedia.com/v1/collect/momo \ -H "Authorization: Bearer wr_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f4b7c1e-invoice-a104" \ -d '{"amount": 10.50, "subscriber_number": "0240000000", "network": "MTN"}' # Retry the exact same request: # -> HTTP/2 201 # -> idempotent-replayed: true # -> body is identical to the first response ``` ## Conflicts | Field | Type | Description | | --- | --- | --- | | Same key, same body, completed | 2xx | Original response is replayed with Idempotent-Replayed: true. | | Same key, same body, still in flight | 409 | A request with this key is still in progress. Wait and retry. | | Same key, different body | 409 | Idempotency-Key reused with a different request body. Pick a new key. | | Same key after 24h | 409 | Key expired. Use a new one. | ## Recovering a lost transaction id If the response to a `POST` is lost in transit you can look the transaction up by the` Idempotency-Key `you sent, without re-issuing the money-moving request: shell ``` curl -H "Authorization: Bearer wr_live_..." \ "https://api.webrabbitmedia.com/v1/transactions?idempotency_key=8f4b7c1e-invoice-a104" # -> { "items": [ { "provider_transaction_id": "521888807466", "status": "approved", ... } ], ... } # -> Empty items[] means we have no record — safe to retry the POST with the same key. ``` ## Best practices - Generate a UUID per business event (invoice id, order id) and reuse it across retries. - Set client-side timeouts to at least 30s — MoMo prompts can be slow. - Retry on network errors and 5xx. Do not retry on 4xx other than 429. - On a lost response, look up by idempotency_key before assuming failure. --- # Rate limits > Per-key and per-IP limits enforced at the Cloudflare edge. The API enforces per-key rate limits at the Cloudflare edge using the native Rate Limiting binding — limits are strongly consistent per data center, so bursts cannot slip through eventual-consistency windows. Payments and Messaging share one contract. ## Limits Limits are per API key, counted in a 10-second window, and apply identically to Payments and Messaging. Every plan has a burst allowance: short spikes above the sustained rate are absorbed, and only sustained traffic above the burst ceiling is rejected. | Field | Type | Description | | --- | --- | --- | | Standard (default) | 120 req / 10 s · burst 240 | Every key starts here. 720 sustained requests a minute. | | Scale | 600 req / 10 s · burst 1,200 | For platforms with evening peaks — ride-hailing, delivery, ticketing. Granted on request, no charge. | | Enterprise | 2,000 req / 10 s · burst 4,000 | Negotiated. Contact us with your peak RPS. | | Status & balance reads | separate budget | Transaction status, campaign delivery reports and balance reads draw on their own pool, so polling never starves your sends. | | Unauthenticated (per IP) | 20 req / 10 s | Protects the 401 path from brute-force key guessing. Also covers GET /v1/health. | › Moving up a tier Email [support@webrabbitmedia.com](mailto:support@webrabbitmedia.com) with your business id and expected peak requests per second. Scale is normally granted the same day and applied to your keys without a redeploy — nothing in your code changes. ## Headers on every response | Field | Type | Description | | --- | --- | --- | | x-ratelimit-limit | integer | Sustained budget for this window. | | x-ratelimit-remaining | integer | Requests left in the current window. | | x-ratelimit-reset | unix seconds | When the window rolls over. | | x-ratelimit-tier | string | standard, scale or enterprise. | | retry-after | seconds | On 429 only. | | x-request-id | uuid | On every response, including errors. Quote it in support requests. | ## Prefer webhooks over polling Signed HMAC-SHA256 [webhooks](https://webrabbitmedia.com/docs/webhooks) are live and are the intended way to learn a transaction's outcome — they remove most polling traffic entirely. Keep a slow reconciliation poll as a safety net, not as your primary path. ## 429 response HTTP/2 429 Too Many Requests ``` HTTP/2 429 content-type: application/json retry-after: 10 x-ratelimit-limit: 120 x-ratelimit-remaining: 0 x-ratelimit-reset: 1789000010 x-ratelimit-tier: standard x-request-id: 6f3c7d1e-9a2b-4c8f-b5e0-2b1a7e9d0c33 { "error": "Rate limit exceeded", "request_id": "6f3c7d1e-9a2b-4c8f-b5e0-2b1a7e9d0c33" } ``` ## Handling - Respect the Retry-After header (seconds). - Use exponential backoff with jitter on repeated 429s. - Pair retries with an Idempotency-Key to keep them safe — see Idempotency. shell ``` # retry once, honouring Retry-After resp=$(curl -s -D /tmp/h -o /tmp/b -w "%{http_code}" \ https://api.webrabbitmedia.com/v1/transactions \ -H "Authorization: Bearer wr_live_...") if [ "$resp" = "429" ]; then wait=$(grep -i '^retry-after:' /tmp/h | tr -dc '0-9') sleep "${wait:-10}" curl -s https://api.webrabbitmedia.com/v1/transactions \ -H "Authorization: Bearer wr_live_..." fi ``` --- # Request IDs & logs > Every response is tagged with an x-request-id; we log it for 30 days. Every response includes an `x-request-id` header. It uniquely identifies the request in our structured logs and metrics. ## Using request ids - Log the id alongside your own request/response records. - Include it verbatim when contacting support — we can look up the full trace in seconds. - You may also send your own x-request-id on the request; we'll honour it end-to-end. shell ``` curl -i https://api.webrabbitmedia.com/v1/health # HTTP/2 200 # x-request-id: 6f3c7d1e-9a2b-4c8f-b5e0-2b1a7e9d0c33 # content-type: application/json # # {"ok":true,"service":"webrabbit-api","request_id":"6f3c7d1e-..."} # send your own id and we echo it back curl -i https://api.webrabbitmedia.com/v1/health \ -H "x-request-id: order-1024-attempt-1" ``` ## Logs & retention > **What we log** Method, path, HTTP status, upstream status, latency, mode (test/live), business id, api key id, rate-limit outcome, idempotency outcome, client IP, and user-agent. Request bodies and secrets are never logged. Logs are retained for 30 days . Aggregate metrics (latency, status class, RPS by mode) are kept for 90 days in Cloudflare Analytics Engine and surfaced in your dashboard. --- # Errors > HTTP status codes, error shapes, and how to recover. The API uses conventional HTTP status codes. `2xx` means success, `4xx` means the request was rejected, and `5xx` means something went wrong on our side. ## Error shape All error responses are JSON. Errors raised at the edge (rate limits, unknown routes, gateway failures) carry `error` and `request_id`: Response · 400 ``` { "error": "amount must be > 0", "request_id": "6f3c7d1e-9a2b-4c8f-b5e0-2b1a7e9d0c33" } ``` | Field | Type | Description | | --- | --- | --- | | error | string | Human-readable message. Safe to log; do not show verbatim to end customers. | | request_id | string | Unique id echoed in the x-request-id response header. Include when contacting support. Present on edge-generated errors. | Errors raised by the payment layer are terser and may add machine-readable fields instead of `request_id` — for example an invalid key returns `{ "error": "Invalid API key" }` and a failed wallet lookup returns `{ "error": "account_not_found", "code": "account_not_found", "reason": "…" }`. The `x-request-id` response header is always present on every response, error or not — read it from the headers rather than the body. ## Status codes - 200 OK — request succeeded (queried resource, resolved failure). - 201 Created — a new resource was created (approved charge or checkout session). - 202 Accepted — request accepted but still pending (e.g. momo prompt awaiting customer). - 400 Bad Request — invalid parameters (amount, network, phone). - 401 Unauthorized — missing, revoked, expired, or invalid API key. - 403 Forbidden — key lacks permission (e.g. live key on unapproved business, read key attempting a charge). - 403 Forbidden — domain_blocked: live requests from a site we have blocked after review. See Identify your app. - 404 Not Found — route does not exist or resource is not yours. - 409 Conflict — Idempotency-Key was reused with a different body, or a matching request is still in flight. - 429 Too Many Requests — rate limit exceeded, retry after the Retry-After header. - 501 Not Implemented — provider_unsupported: the payout endpoints are retired; payouts run from the dashboard. - 502 Bad Gateway — the upstream payment provider rejected or could not process the request. The transaction is recorded as failed with the provider's reason. - 5xx — retry idempotent requests with exponential backoff and the same Idempotency-Key. The upstream provider's `code` field (e.g. `"01"` for a declined MoMo charge) is separate from the HTTP status — see [Provider codes](https://webrabbitmedia.com/docs/provider-codes). ## Attribution warnings Successful responses may include a `x-wr-attribution-warning` header when your request did not declare `HTTP-Referer` and `X-Webrabbitmedia-Title`. It is a warning, not an error — the request still processed — but add the headers so your integration stays reviewable. See [Identify your app](https://webrabbitmedia.com/docs/authentication#identify-your-app). ## Handling errors in code A call made with an invalid key returns `401`. Always branch on the HTTP status first and log `x-request-id` from the response headers. shell ``` curl -i https://api.webrabbitmedia.com/v1/me \ -H "Authorization: Bearer wr_test_not_a_real_key" # HTTP/2 401 # x-request-id: 6f3c7d1e-9a2b-4c8f-b5e0-2b1a7e9d0c33 # # {"error":"Invalid API key"} ``` --- # Fees > How the platform fee and the payout fee are calculated and settled — 10% starting rate, 1.5% on withdrawals. Web Rabbit charges a platform fee on every successful collection. There are no monthly minimums, setup fees, or hidden charges. ## Platform fee Web Rabbit Processing starts at 10% of the gross amount collected. It is deducted from every successful charge before the net is credited to your available balance. Rates may vary depending on business model, transaction volume, risk profile and payment requirements, and accounts opened before the 10% starting rate keep the rate already agreed with them. › Formula `net = gross − (gross × rate)` Your own rate is always returned as `commission_bps` on `GET /v1/me` — `1000` means 10%. Read it from there rather than hard-coding a percentage. ## Worked example You charge a customer GHS 100.00 via MTN Mobile Money on the 10% starting rate. - Gross: 100.00 - Platform fee (10%): 10.00 - Net to your balance: 90.00 The full breakdown is visible on every transaction in the dashboard and returned on every API response as `gross_amount`, `fee_amount`, and `net_amount`. ## Payout fee Withdrawing settled funds costs 1.5% of the amount withdrawn — flat, with no minimum or maximum. The fee is deducted from the amount you request, so a GHS 10,000.00 withdrawal pays out GHS 9,850.00 and reduces your balance by GHS 10,000.00. - Amount requested: 10,000.00 - Payout fee (1.5%): 150.00 - Paid to your destination: 9,850.00 Every payout record shows the fee, the amount paid out, and your balance before and after the payout, in the dashboard under Payouts → History. Bills carry no platform fee (bill payments are currently unavailable). Airtime, data bundles and bill payments bought through [Bills & Airtime](https://webrabbitmedia.com/docs/bills) debit exactly the amount purchased, and a failed purchase is refunded in full. › Only charged on approved The fee is only deducted on `approved` collections. Pending charges show `fee_amount = 0` until they resolve, and failed charges never accrue a fee. --- # Products, orders & inventory > Sell from a permanent product link and manage orders and stock. Products lets you add a physical item, digital product, or service, publish a permanent shareable link, accept payment through Hosted Checkout, and manage the resulting order. ## Create a product Open [Sales → Products](https://webrabbitmedia.com/merchant/sales/products), add a name, price and description, then choose whether stock is limited or unlimited. Physical products can collect a delivery address and add a fixed delivery fee. Draft products are private; publishing enables the product link. ## Share the product link Copy the link from the product list and share it anywhere. The link stays the same when you edit the name, price, image, stock, or delivery settings. Archiving the product stops new orders without deleting its history. ## Inventory and reservations Limited stock is reserved for 15 minutes while a customer checks out. A successful payment commits the reservation and reduces stock exactly once; an expired or failed checkout releases it. Use Stock to record restocks or corrections with a reason. Unlimited products do not track a quantity. > **Refunds and stock** A full successful refund restores limited stock once and marks an unfulfilled order cancelled. Partial refunds do not change stock automatically. ## Manage orders [Sales → Orders](https://webrabbitmedia.com/merchant/sales/orders) shows payment and fulfilment separately. Open an order to see the customer, delivery address and immutable item-price snapshot, then move paid orders through processing, ready, shipped, picked up, or fulfilled. Search, status filters, test/live isolation and CSV export are included. ## Webhook events Subscribe to `order.paid`, `order.failed`, `order.fulfilled`, and`inventory.low`. Order payment events include the order id, order number, payment status, and transaction id. Low-stock events include the product id and remaining quantity. --- # Mobile Money > Charge a customer over MTN, Telecel, or AirtelTigo. Charge a customer over Mobile Money. The customer receives a prompt on their phone to authorise the payment, so the charge always starts as `pending` and settles asynchronously. > **Requires write access** The API key must have `write` access. Read-only keys receive `403 insufficient_scope`. ## Endpoint POST `/v1/collect/momo` > **Supported networks** Pass the `network` field using one of these codes: `MTN` · `TELECEL` (formerly Vodafone) · `AT` (AirtelTigo) · `GMONEY`. Legacy codes `VDF`, `ATL` and `TGO` are still accepted and mapped automatically. See [institution codes](https://webrabbitmedia.com/docs/provider-codes). `subscriber_number` accepts either the local format `0240000000` or the international format `233240000000` — we normalise it for the provider. `amount` is decimal GHS — no pesewa padding. See [test numbers](https://webrabbitmedia.com/docs/test-data). › Wallets are name-verified first Before any debit we run the provider's mandatory name lookup on the wallet. If the number cannot be resolved you get `422 account_not_found` and no transaction is created . On success the resolved `account_name` is returned and stored with the transaction. This applies to every upstream provider; the lookup wording in `reason` differs between OGateway, 360Pay and JuniPay. > **Upstream rejections return 502** If the provider accepts the request shape but refuses the charge, the transaction is recorded as `failed` and the endpoint answers `502` with the provider's own reason in `reason`. Retry with the same `Idempotency-Key`. ## Request | Field | Type | Description | | --- | --- | --- | | amount required | number | Amount in GHS. Accepts number or numeric string, e.g. 10.50. | | subscriber_number required | string | Customer phone number, "0240000000" or "233240000000". | | network required | enum | One of MTN, TELECEL, AT, GMONEY. | | desc | string | Description shown in your dashboard. Max 100 chars. | | customer_email | string | Optional email captured with the transaction. | Send an `Idempotency-Key` header to make retries safe — see [Idempotency](https://webrabbitmedia.com/docs/idempotency). shell ``` curl -X POST https://api.webrabbitmedia.com/v1/collect/momo \ -H "Authorization: Bearer wr_test_..." \ -H "Content-Type: application/json" \ -H "HTTP-Referer: https://your-app-site.com" \ -H "X-Webrabbitmedia-Title: Your App Name" \ -H "Idempotency-Key: 8f4b7c1e-invoice-a104" \ -d '{ "amount": 10.50, "subscriber_number": "0240000000", "network": "MTN", "desc": "Invoice A104", "customer_email": "customer@example.com" }' ``` ## Response Response · 202 Pending ``` { "transaction_id": "521888807466", "provider_transaction_id": "TXN-8841002", "status": "pending", "code": "02", "reason_code": "prompt_sent", "reason": "Transaction is being processed", "account_name": "AMA SERWAA", "subscriber_number": "0240000000", "gross_amount": 10.5, "fee_amount": 0, "net_amount": 10.5, "currency": "GHS" } ``` | Field | Type | Description | | --- | --- | --- | | transaction_id | string | 12-digit Web Rabbit transaction id. Use this to poll status. | | provider_transaction_id | string | Provider-side transaction id for the collection. | | status | enum | pending · approved · failed. MoMo charges start pending. | | code | string | Upstream status code: "00" success, "01" failed, "02" pending, "03" processing. | | account_name | string | Wallet holder name resolved by the mandatory name lookup. | | gross_amount | number | Amount charged, before fee. | | fee_amount | number | Platform fee at your account rate, applied once the charge is approved. | | net_amount | number | Amount credited to your balance after settlement. | | currency | string | Always "GHS" today. | › HTTP status `202` for pending (the usual case — the customer still has to authorise on their phone),`201` if the provider settles immediately, `200` for a resolved failure, `422` when the wallet fails name verification. All responses include an `x-request-id` header — save it if you need support. The final outcome is confirmed by [polling the retrieve endpoint](https://webrabbitmedia.com/docs/webhooks). > **Test mode is fully simulated** `wr_test_` keys are answered by our built-in simulator — no provider call, no prompt on anyone's handset, no money moved, and amounts capped at GHS 10. Outcomes are driven by the amount, so CI runs are deterministic. See the trigger table in [Test data](https://webrabbitmedia.com/docs/test-data). --- # Hosted Checkout > Create a hosted payment page that accepts card and Mobile Money. Hosted Checkout is the fastest way to accept Mobile Money (and bank transfer in Nigeria). You create a session, we return a secure payment page URL, and you redirect the customer to it. > **Card payments are currently unavailable** Card payments are switched off. Sessions created with `channel: "CARD"` or via the legacy card path open with Mobile Money instead. Card details on this page are kept for reference. > **Requires write access** The API key must have `write` access. Read-only keys receive `403 insufficient_scope`. ## Endpoint POST `/v1/checkout/session` The legacy path `/v1/collect/card` is an alias for this endpoint. Raw card (PAN) charges are no longer accepted. > **Payout endpoints are retired** `POST /v1/payout/momo` and `POST /v1/payout/bank` now return `501 provider_unsupported`. Payouts are initiated manually from [Payouts](https://webrabbitmedia.com/merchant/payouts) in the dashboard. ## Request | Field | Type | Description | | --- | --- | --- | | amount required | number | Total to charge, in GHS. | | customer_email | string | Optional payer email (and customer_name). Mobile Money and bank transfer never ask for contact details; card asks for name and email only if you didn't send them. Send them to pre-fill. | | channel | enum | ANY (default), MOMO, or CARD — recorded on the transaction. | | network | enum | Optional MTN, TELECEL, AT or GMONEY — preselects the wallet on the page. | | subscriber_number | string | Optional payer phone number, prefilled on the page. | | desc | string | Order description shown in your dashboard. Max 100 chars. | | reference | string | Your order or player reference. Returned on the session, the transaction and every collection.* webhook. Max 120 chars. | | metadata | object | Up to 20 flat string/number/boolean keys (2 KB). Returned in collection.* webhooks with checkout_session_id. | | currency / country | string | Optional. Must match your business's registered country (see GET /v1/me → enabled). Anything else returns 422 currency_not_enabled. | shell ``` curl -X POST https://api.webrabbitmedia.com/v1/checkout/session \ -H "Authorization: Bearer wr_test_..." \ -H "Content-Type: application/json" \ -H "HTTP-Referer: https://your-app-site.com" \ -H "X-Webrabbitmedia-Title: Your App Name" \ -d '{ "amount": 170.00, "customer_email": "customer@example.com", "channel": "ANY", "network": "MTN", "subscriber_number": "0240000000", "desc": "Order 1042" }' ``` ## Response Response · 201 Created ``` { "id": "cs_7Qk2mXv9Lp", "status": "open", "checkout_url": "https://webrabbitmedia.com/pay/cs_7Qk2mXv9Lp", "access_code": "cs_7Qk2mXv9Lp", "gross_amount": 170, "currency": "GHS", "expires_at": "2026-08-24T21:26:00.000Z", "mode": "test" } ``` | Field | Type | Description | | --- | --- | --- | | id | string | Checkout session id — also the token in the checkout URL. | | checkout_url | string | Web Rabbit branded checkout page. Redirect the customer here. | | access_code | string | Same value as id, kept for backwards compatibility. | | gross_amount | number | Amount the customer is charged, in GHS. | | expires_at | string | ISO timestamp. Sessions expire 15 minutes after creation. | | mode | enum | test or live — taken from the API key used to create the session. | | status | enum | open at creation, then processing, paid, failed, expired or cancelled. | ## Session lifecycle The page is hosted by Web Rabbit, not by the processor. The customer goes straight to choosing a payment method. Mobile Money (charged natively on the page) and bank transfer need only the wallet number; name, email and billing address are optional receipt details. Card asks for name and email (unless pre-filled) and then opens the processor's PCI DSS certified page in a new tab. Sessions in a currency that isn't open for your account are refused at creation with `422 currency_`. Check `enabled.countries[].provider_status` on `GET /v1/me`: `enabled`, `opens_later` (with `provider_opens_at`), `pending_provider`, `blocked_high_risk` (the provider doesn't accept betting/gaming in that currency) or `pending_onboarding` (Web Rabbit is finishing provider onboarding for your business). Some currencies have a minimum amount, shown as `min_amount` per country on `GET /v1/me` (for example NGN 500). Smaller sessions are refused with `422 amount_below_minimum`. When the payment settles, the customer lands on the branded result page and is redirected to your `success_url` after a short countdown. | Field | Type | Description | | --- | --- | --- | | Expiry | 15 minutes | A countdown runs on the page; an expired link cannot be paid and must be re-created. | | Test mode | wr_test_ keys | The page shows a Test mode badge. Both Mobile Money and card are answered by our built-in simulator — nothing reaches the processor, no money moves, and the GHS 10 test cap applies. | | Retrieve | GET | GET /v1/checkout/session?id= returns the current session and its transaction. | › Confirm before fulfilling Never fulfil an order on the redirect alone. Confirm with [GET /v1/transactions/{id}](https://webrabbitmedia.com/docs/transactions-retrieve) and check `resolved_status === "approved"`. The platform fee is applied at settlement. --- # Split payments & sub-accounts > Divide a collection between your platform and the people you pay, at capture. Split payments let a platform divide a single collection between itself and the people it pays — drivers, couriers, vendors, restaurants — at the moment of capture. You create a sub-account for each recipient, attach a split instruction to the charge, and each leg is credited to the recipient's ledger when the payment settles and clawed back automatically if it is later reversed. > **Built for marketplaces** You remain the merchant of record. Splits are an accounting and payout convenience inside your workspace, not a licence to hold funds belonging to third parties — see the [Merchant Acceptance Policy](https://webrabbitmedia.com/docs/merchant-acceptance) for the conditions that apply to marketplace models. ## Sub-accounts A sub-account is a named settlement destination scoped to your business and to one mode. The same `reference` can exist in test and live independently. POST `/v1/sub-accounts` | Field | Type | Description | | --- | --- | --- | | name | string · required | Human name shown in your dashboard, e.g. "Kwame Mensah". | | reference | string | Your own stable id — letters, digits, - and _. Derived from name if omitted. | | settlement_method | enum | momo (default) or bank. | | account_number | string · required | Wallet number (0XXXXXXXXX) for momo, account number for bank. | | network | enum | Required for momo: MTN · AT · TELECEL · GMONEY. | | bank_code | string | Required for bank sub-accounts. | | account_name | string | Optional name on the destination account. | shell ``` curl -X POST https://api.webrabbitmedia.com/v1/sub-accounts \ -H "Authorization: Bearer wr_test_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Kwame Mensah", "reference": "driver_8814", "settlement_method": "momo", "account_number": "0244000000", "network": "MTN" }' ``` 201 Created ``` { "id": "0f2c…", "reference": "driver_8814", "name": "Kwame Mensah", "status": "unverified", "settlement_method": "momo", "account_number": "0244000000", "network": "MTN", "mode": "test", "balance": 0, "balance_minor": 0, "currency": "GHS" } ``` ## List, retrieve, update, ledger | Field | Type | Description | | --- | --- | --- | | GET /v1/sub-accounts | list | All sub-accounts for the key's mode, newest first, with live balances. | | GET /v1/sub-accounts/{reference} | retrieve | One sub-account by reference or id. | | PATCH /v1/sub-accounts/{reference} | update | Change name, destination, or set status to disabled / unverified. | | GET /v1/sub-accounts/{reference}/ledger | ledger | Credit and reversal entries with running balance. limit 1–200. | > **Changing a destination re-opens verification** Updating `account_number` or `network` resets the sub-account to `unverified`. Only Web Rabbit sets `verified`; you can set `disabled` to stop new splits landing on it. ## Splitting a charge Add a `split` object to `POST /v1/collect/momo` or `POST /v1/checkout/session`. Each recipient is either a percentage of the collection or a fixed amount, and whatever is left over stays with you. | Field | Type | Description | | --- | --- | --- | | split.fee_bearer | enum | merchant (default) — the Web Rabbit fee comes out of your share. recipients — the fee is taken off the top before percentages are applied. | | split.recipients[].sub_account | string · required | Sub-account reference or id, in the same mode as the key. | | split.recipients[].percentage | number | Share of the split base, 0–100. Use this or amount, not both. | | split.recipients[].amount / amount_minor | number / integer | Fixed leg in GHS or pesewas. | POST /v1/collect/momo ``` { "amount_minor": 5000, "subscriber_number": "0244000000", "network": "MTN", "description": "Trip 88142", "split": { "fee_bearer": "merchant", "recipients": [ { "sub_account": "driver_8814", "percentage": 85 } ] } } ``` Preview the arithmetic before you charge — useful in tests and for showing a driver their payout before the trip is confirmed. POST `/v1/splits/preview` 200 OK ``` { "currency": "GHS", "gross_amount": 50, "gross_amount_minor": 5000, "fee_amount": 7.5, "fee_amount_minor": 750, "net_amount": 42.5, "net_amount_minor": 4250, "fee_bearer": "merchant", "merchant_amount": 0, "merchant_amount_minor": 0, "recipients": [ { "sub_account": "driver_8814", "type": "percentage", "value": 85, "amount": 42.5, "amount_minor": 4250 } ] } ``` > **Integer arithmetic, always** Legs are computed in pesewas. Any rounding remainder is added to the largest leg, so the legs plus your share always equal the net amount exactly. ## Settlement and reversal | Field | Type | Description | | --- | --- | --- | | pending | split_status | Instruction recorded, payment not yet approved. Nothing has been credited. | | settled | split_status | Payment approved — each leg is credited to its sub-account ledger once, and split.settled fires. | | reversed | split_status | Payment was clawed back — every credited leg is debited back and split.reversed fires. | `GET /v1/transactions/{id}` returns `split_status` and the full `splits` array, so reconciliation never has to be inferred. > **Size your payout hold against the reversal window** Mobile Money collections can be reversed for up to 7 days after settlement; card collections carry chargeback exposure for up to 120 days . If you pay recipients out faster than that, you are carrying the reversal risk yourself. ## Webhook events | Field | Type | Description | | --- | --- | --- | | split.settled | event | Fired once per transaction when all legs are credited. Includes every leg with amount_minor. | | split.reversed | event | Fired when a settled collection is reversed and the legs are debited back. | --- # Refunds > Send money back to the wallet that paid, in full or in part, from your dashboard. The Refunds section lets you manage and send refunds for your transactions. You can start full or partial refunds directly from the payment details view, with built-in rules that keep every refund compliant and protect you from mistakes. > **A refund is a transfer, not a reversal** Neither of the rails we settle on exposes a refund or reversal endpoint. So we do not undo the original charge — we send a fresh outbound transfer back to the customer's wallet and link it to the payment. The customer receives the money as a new mobile money credit. ## Initiate refund You can start two kinds of refund from the payment details page ( Transactions → Payments , open a successful payment): - Full refund — sends the entire remaining payment amount back to the customer. - Partial refund — sends only the amount you choose. Partial refunds can be repeated until the original payment is fully returned. Click Initiate refund to begin. The refund only proceeds if every refund rule below is met — the same rules are checked again on our servers when you confirm. > **Tip** Partial refunds are useful when only part of an order needs to be returned — for example one item from a larger purchase. > **Refunds in local currencies** A refund is always sent in the original payment's currency, from your balance in that same currency. For example, a KES payment is refunded from your KES balance, never from GHS. The refund charge and daily value cap are set in GHS and converted into the payment's currency using the admin limit rates. Daily caps are counted separately for each currency. Mobile money payments outside Ghana are refunded to the same wallet in the same country. Where automatic refunds aren't available yet (for example Nigerian bank-transfer payments), the refund is held and our team sends it manually. ### Details required for a refund | Field | Type | Description | | --- | --- | --- | | Refund amount | required | How much to send back. For a partial refund, enter the exact amount. The system never lets you refund more than the original payment. | | Refund reason | optional | Recorded on the refund and shown on the customer receipt, so keep it clear and customer-friendly. | ### Refund rules - The payment must have been successful and settled. Pending, failed, reversed and corrected payments cannot be refunded. - Refunds (full or partial) must be started within 30 days of the transaction. After that the refund option closes — contact support for anything older. - Refunds go back to the original payment method — the exact mobile money wallet the customer paid with. Card or bank-funded payments need a manual refund via support. - Your available balance must cover the refund plus the refund charge. - Only the business owner can issue refunds. Team admins see refund history read-only. If any rule is violated the refund is denied with a specific reason. Reach out to support if you need help with a payment that falls outside these rules. ## Refund status Track each refund through these status indicators: | Field | Type | Description | | --- | --- | --- | | succeeded | status | The refund was processed and the customer received the money. | | pending | status | Accepted and about to be sent to the rail. | | processing | status | Handed to the rail. The outcome always comes from a verified status check, never the rail acknowledgement. | | review | status | Held for a quick Web Rabbit review before anything is sent. Possible reasons: risk or compliance check, daily refund count cap (25 by default) or daily value cap (GHS 5,000 by default) exceeded, or refunds on hold for AML/fraud reasons. | | failed | status | The refund could not be processed. Possible reasons: insufficient balance in your wallet, the rail declined the transfer, or a network issue. Nothing left your balance. | | cancelled | status | Cancelled during review or by a concurrency guard when two refunds raced for the same payment. | ## Refunds page The Refunds page (Transactions → Refunds) lists every refund in the mode you are viewing — test and live refunds are never mixed: - Search by refund ID, associated payment ID, destination number or reason. - Filter by status (successful, processing, in review, failed). - The Associated payment column links each refund to its original payment — open it from Transactions → Payments for the full details. - Each successful refund shows whether the customer receipt was sent by email, SMS, or both. - Export CSV downloads the visible (filtered) rows for your records or accounting. ## Refund receipt When a refund succeeds, a refund receipt is sent to the customer automatically: - Email — sent whenever the original payment captured a customer email address (checkout and collections both support this). No setup needed. - SMS — optional. Turn on SMS refund receipts in Messaging → Settings → Defaults and the customer gets a text on the wallet they paid with. Charged to your messaging balance at the normal destination rate; if the balance is too low the SMS is skipped and the refund is never held up. The receipt includes: - Refund ID — the unique reference for the refund. - Refund amount — the amount returned to the customer. - Refund date — when the refund was processed. - Refund method — the original wallet and network. - Refund status — successful. - Refund reason — the reason you entered when starting the refund. Test mode simulates receipts without sending real email or SMS, so you can rehearse the full flow safely. ## Fees The platform commission on the original payment is not returned. A flat refund charge (GHS 2 by default) is taken from your balance per refund. Your balance is reduced by the refund amount plus that charge. ## The 30-day window The window runs for 30 days from the moment the payment settled, counted to the second, and applies in test mode exactly as it does in live mode. A refund that was started inside the window keeps processing even if it completes later, and a refund parked in `review` can still be released by Web Rabbit after the 30 days. The limit is a per-merchant setting, so if your business needs a longer window, talk to us. ## Webhook events Three signed events are sent, with the refund nested under `data.object` like every other event: refund.succeeded ``` { "id": "evt_...", "type": "refund.succeeded", "mode": "live", "data": { "object": { "refund_id": "730114552901", "transaction_id": "521888807466", "status": "succeeded", "type": "partial", "amount": 40, "amount_minor": 4000, "fee_amount": 2, "currency": "GHS", "destination": "233240000000", "network": "MTN", "reason_code": "success" }, "resource_type": "refund" } } ``` `refund.created` fires the moment a refund is accepted, `refund.succeeded` and `refund.failed` only after a verified provider verdict. ## Test mode Refunds in test mode run entirely on the simulator, capped at GHS 10, and never touch a real rail or move real money. Receipts are simulated too. > **No public refund API yet** Refunds move money out of your balance, so today they are owner-authenticated dashboard actions only — there is no API-key endpoint. If you need programmatic refunds, contact us and we will scope it with you. --- # Disbursements (payouts to third parties) > Batch payouts to drivers, vendors and couriers, with release tiers and review. Disbursements pay money out of your settled Web Rabbit balance to third parties — drivers, couriers, vendors, restaurants, affiliates — in batches of up to 500 recipients, over mobile money or bank transfer. This is the payout rail for platforms; the [split payments](https://webrabbitmedia.com/docs/split-payments) API decides who is owed what, and disbursements move that money. > **Switched on per business** Disbursements are off by default. An admin enables them on your business after review. Until then every call returns `403 disbursements_not_enabled`. Ask us in the dashboard or by email and we will review your volumes, your recipient list and your model. ## Release tiers A disbursement is never released the moment you create it. Money leaving a balance to arbitrary recipients is the highest-risk flow we operate, so each business sits in a tier that sets how long a batch settles for and how much human review it passes through. | Field | Type | Description | | --- | --- | --- | | individual | T+7 · reviewed | Sole proprietors and creators. Every batch is reviewed by a person. One batch per rolling 7 days. Minimum batch GHS 100. | | business | T+3 · reviewed | Registered entities with completed business verification. Every batch is reviewed. One batch per rolling 7 days. Minimum batch GHS 50. | | powered | T+1 · automatic | Businesses built and vetted end to end by Web Rabbit Media. Approved automatically, no weekly cap, minimum batch GHS 20. Still subject to holds and clawback. | The tier on your business is returned on every list call as `tier`, alongside `disbursements_enabled`. We may hold or reverse a batch at any point if activity looks like fraud, money laundering, or payouts to people your platform cannot account for. ## Create a disbursement POST `/v1/disbursements` | Field | Type | Description | | --- | --- | --- | | channel | enum | momo (default) or bank. One channel per batch. | | reference | string | Your own label for the batch, e.g. "friday-riders". | | recipients | array · required | 1–500 transfers. Each needs a name and an amount. | | recipients[].name | string · required | Name of the person or business being paid. | | recipients[].msisdn | string | Wallet number for channel=momo, e.g. 0244000000. | | recipients[].network | enum | Required for momo: MTN · TELECEL · AT · GMONEY. | | recipients[].account_number | string | Account number for channel=bank. | | recipients[].bank_code | string | Required for channel=bank. | | recipients[].amount | number | GHS. Send this or amount_minor, not both. | | recipients[].amount_minor | integer | Pesewas. Exact — preferred machine to machine. | | recipients[].reference | string | Your reference for this transfer. Generated if omitted. | | recipients[].narration | string | Optional text shown to the recipient where the rail supports it. | Send an `Idempotency-Key` header (or `idempotency_key` in the body). A retry with the same key returns the original batch instead of paying anyone twice. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/disbursements \ -H "Authorization: Bearer wr_test_..." \ -H "Idempotency-Key: friday-riders-2026-09-11" \ -H "Content-Type: application/json" \ -d '{ "channel": "momo", "reference": "friday-riders", "recipients": [ { "reference": "driver_8814", "name": "Kwame Mensah", "msisdn": "0244000000", "network": "MTN", "amount": 120 }, { "reference": "driver_8815", "name": "Ama Boateng", "msisdn": "0559876543", "network": "TELECEL", "amount_minor": 8500 } ] }' ``` 201 Created ``` { "id": "94afd213-8dbe-450e-958d-53b6d8f852f8", "object": "disbursement", "status": "pending_review", "mode": "live", "channel": "momo", "tier": "business", "currency": "GHS", "amount": 205, "amount_minor": 20500, "fee": 3.08, "item_count": 2, "reference": "friday-riders", "scheduled_for": "2026-09-14T09:12:04.000Z", "released_at": null, "created_at": "2026-09-11T09:12:04.000Z", "recipients": [ { "id": "6f1a...", "reference": "driver_8814", "status": "queued", "name": "Kwame Mensah", "account_number": "0244000000", "network": "MTN", "amount": 120, "amount_minor": 12000, "fee": 1.8 } ] } ``` > **Fees** Each transfer costs 1.5% of its amount, at least GHS 1 and at most GHS 50. You're only charged for transfers that go through, so failed transfers are free. The `fee` on each recipient is the fee quoted when you submitted the batch. > **Working days only** Disbursements are released on working days only — Monday to Friday, excluding Ghana public holidays. A batch whose release date lands on a weekend or holiday moves to the next working day; `scheduled_for` on the batch always shows the actual release date. Batch amounts also count toward your daily and monthly payout limits, same as withdrawals. ## Statuses | Field | Type | Description | | --- | --- | --- | | pending_review | batch | Waiting for a person to approve it. Individual and business tiers. | | approved | batch | Cleared, waiting for its release date. | | declined | batch | Rejected on review. See decision_reason. Nothing was paid. | | held | batch | Paused while we look into it. See hold_reason. | | processing | batch | Released and being paid out recipient by recipient. | | completed | batch | Every recipient succeeded. | | partially_completed | batch | Some recipients failed. Check each recipient. | | failed | batch | Every recipient failed. Funds return to your balance. | | cancelled | batch | You cancelled it before release. | | queued / processing / succeeded / failed | recipient | Per-transfer status inside the batch. | ## List, retrieve and cancel GET `/v1/disbursements` Newest first, in the key's mode, with opaque cursors — pass `?cursor=` from `next_cursor`. The response also carries your `tier` and `disbursements_enabled`. GET `/v1/disbursements/{id}` POST `/v1/disbursements/{id}/cancel` A batch can be cancelled any time before it is released. Once it is processing, cancellation returns an error — individual failed transfers are returned to your balance automatically. ## Test mode Test disbursements are fully simulated: no rail is contacted and no money moves. Wallet numbers ending `0000` succeed, `1111` fail, and the GHS 10 test-mode amount cap applies per recipient. Test batches skip the tier clock so you can see the whole lifecycle immediately. See [Test data](https://webrabbitmedia.com/docs/test-data). ## Webhook events | Field | Type | Description | | --- | --- | --- | | payout.queued | event | A batch was created and is awaiting review or its release date. | | payout.completed | event | A transfer succeeded. Sent per recipient. | | payout.failed | event | A transfer failed, with the mapped provider code and reason. | Events are signed exactly like every other Web Rabbit webhook — see [Webhooks](https://webrabbitmedia.com/docs/webhooks) for verification. --- # Bills & Airtime > Currently unavailable — airtime, data bundles and bill payments are switched off. > **Currently unavailable** Airtime, data bundles and bill payments are switched off. Requests to `POST /v1/bills` return `410`. This page is kept for reference in case the service returns. Bills buys airtime, data bundles and utility bill payments straight from your settled Web Rabbit balance — the same balance you withdraw from. One endpoint covers the whole flow: list what is available, resolve an account name, buy, then poll or listen for the result. > **Paid from your settled balance** A purchase debits your settled balance at the moment it is accepted, so money already committed to a bill can never be withdrawn at the same time. We charge no platform fee on bills — `fee` is `0` and `total` equals `amount`. If the purchase fails, the full amount is returned to your balance automatically. ## Endpoint POST `/v1/bills` Authenticate with your secret Payments key, exactly as on [collections](https://webrabbitmedia.com/docs/collect-momo). The `action` field selects the operation; the mode (test or live) comes from the key. | Field | Type | Description | | --- | --- | --- | | catalog | action | Networks we support and the billers you can pay. | | bundles | action | Data bundles for one network, with ids and prices. | | lookup | action | Resolve the account name behind a bill account number. | | purchase | action | Buy airtime, a data bundle or pay a bill. Requires a write key. | | status | action | Refresh one purchase from the provider by id or reference. | | list | action | Page through your purchase history, newest first. Default action. | ## Request fields | Field | Type | Description | | --- | --- | --- | | action | enum · required | catalog · bundles · lookup · purchase · status · list. | | kind | enum | airtime, data or bill. Required for action=purchase. | | amount | number | Cedis. Required for action=purchase and must be greater than zero. | | account_number | string | Mobile number for airtime and data; the bill account number for kind=bill. | | network | enum | MTN · AT · TELECEL. Required for airtime, data and action=bundles. telco is accepted as an alias. | | data_id | string | Bundle id taken from action=bundles. Required for kind=data. | | biller_slug | string | Biller identifier from action=catalog. Required for kind=bill and action=lookup. | | account_name | string | Optional label. Use the value returned by action=lookup where you have it. | | narration | string | Up to 100 characters, stored on the purchase and shown in your history. | | id · reference | string | For action=status. Pass either the purchase id or the reference we returned. | | status · kind | filter | For action=list. Also accepts page (default 1) and limit (1–100, default 25). | ## Buy airtime shell ``` curl -X POST https://api.webrabbitmedia.com/v1/bills \ -H "Authorization: Bearer wr_test_..." \ -H "Idempotency-Key: airtime-0240000000-2026-09-19" \ -H "Content-Type: application/json" \ -d '{ "action": "purchase", "kind": "airtime", "network": "MTN", "account_number": "0240000000", "amount": 5 }' ``` 200 OK ``` { "bill": { "id": "9f2a5f0e-6b51-4a4b-9a4f-2f0c6e1b7d30", "reference": "air_8FQ2K7X1", "kind": "airtime", "status": "pending", "amount": 5, "fee": 0, "total": 5, "currency": "GHS", "biller_slug": "mtn-airtime", "biller_name": "MTN Airtime", "account_number": "0240000000", "account_name": null, "bundle_name": null, "data_id": null, "narration": null, "mode": "live", "message": null, "created_at": "2026-09-19T08:12:04.000Z", "completed_at": null } } ``` ## Buy a data bundle Bundles are priced by the network, so read the list first and send the `data_id` and its price back unchanged. shell ``` # 1. list bundles curl -X POST https://api.webrabbitmedia.com/v1/bills \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "action": "bundles", "network": "MTN" }' # 2. buy one curl -X POST https://api.webrabbitmedia.com/v1/bills \ -H "Authorization: Bearer wr_live_..." \ -H "Idempotency-Key: data-0240000000-2026-09-19" \ -H "Content-Type: application/json" \ -d '{ "action": "purchase", "kind": "data", "network": "MTN", "account_number": "0240000000", "data_id": "mtn-1gb-30d", "bundle_name": "1GB · 30 days", "amount": 12 }' ``` ## Pay a bill Take the `biller_slug` from `action: "catalog"`, confirm the customer with `action: "lookup"`, then purchase. A lookup returns `{ "ok": false, "reason": "..." }` with HTTP 200 when the account cannot be resolved — check `ok`, not the status code. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/bills \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "action": "lookup", "biller_slug": "ecg-postpaid", "account_number": "0200123456" }' curl -X POST https://api.webrabbitmedia.com/v1/bills \ -H "Authorization: Bearer wr_live_..." \ -H "Idempotency-Key: ecg-0200123456-sep" \ -H "Content-Type: application/json" \ -d '{ "action": "purchase", "kind": "bill", "biller_slug": "ecg-postpaid", "account_number": "0200123456", "account_name": "Kwame Mensah", "phone_number": "0240000000", "amount": 150, "narration": "September electricity" }' ``` ## Statuses and settlement > **200 means accepted, not delivered** Every purchase starts `pending`. The network confirms delivery seconds to minutes later. Never treat the create response as proof that airtime landed — wait for `approved`. | Field | Type | Description | | --- | --- | --- | | pending | status | Accepted and debited from your balance. Awaiting the network. | | approved | status | Delivered. completed_at is set and bill.completed fires. | | failed | status | Not delivered. The full amount is refunded to your balance and bill.failed fires. | Refresh a single purchase at any time — this pulls a fresh answer from the provider rather than reading a cache: shell ``` curl -X POST https://api.webrabbitmedia.com/v1/bills \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "action": "status", "reference": "air_8FQ2K7X1" }' ``` ## History `action: "list"` returns `bills`, `total`, `page` and `limit`, newest first, scoped to the key's mode. Filter with `kind` and `status`. The same history is in your dashboard under Bills → History , with CSV export. ## Idempotency Send an `Idempotency-Key` header on every purchase. Replaying the same key with the same body returns the original purchase with `idempotent-replayed: true`; reusing it with a different body returns `409 idempotency_conflict`. See [Idempotency](https://webrabbitmedia.com/docs/idempotency). ## Errors | Field | Type | Description | | --- | --- | --- | | 400 invalid_request | error | Missing or unusable kind, amount, network, data_id or biller_slug. | | 402 insufficient_balance | error | Your settled balance does not cover the purchase. Nothing was bought. | | 403 insufficient_scope | error | Read-only key, or the business is not approved for live mode. | | 404 not_found | error | Unknown id or reference on action=status. | | 409 idempotency_conflict | error | Idempotency-Key reused with a different body, or still in flight. | | 429 rate_limited | error | Too many requests. Back off and retry — see Rate limits. | | 502 provider_error | error | The provider rejected the purchase. Your balance is refunded in full. | ## Test mode A test key runs entirely on our simulator: no provider is contacted, no airtime is sent and no real money moves. Send GHS 2.00 to force a failed purchase and a refund; any other positive amount settles as pending and then approved. See [Test data](https://webrabbitmedia.com/docs/test-data). ## Webhook events | Field | Type | Description | | --- | --- | --- | | bill.completed | event | The purchase was delivered. Payload carries the full bill object. | | bill.failed | event | The purchase failed and your balance was refunded in full. | Both events are signed and retried like every other event — see [Webhooks](https://webrabbitmedia.com/docs/webhooks). --- # List transactions > Paginate through every transaction for your business. Paginate through every transaction for the business tied to your API key, newest first. Results are automatically scoped to the key's mode (test or live). > **Requires read access** Any valid key (read or read + write) can call this endpoint. ## Endpoint GET `/v1/transactions` ## Query parameters | Field | Type | Description | | --- | --- | --- | | limit | integer | Page size, 1–100. Defaults to 25. | | cursor | string · opaque | Pass back the next_cursor from the previous response verbatim. Do not construct or parse it. | | status | enum | Filter by status: approved · pending · failed. | | channel | enum | momo · card. | | type | enum | collection. | | from | string · ISO 8601 | Inclusive lower bound on created_at. | | to | string · ISO 8601 | Inclusive upper bound on created_at. | ## Request shell ``` curl -G https://api.webrabbitmedia.com/v1/transactions \ -H "Authorization: Bearer wr_test_..." \ -d limit=25 \ -d status=approved \ -d channel=momo ``` ## Response Response · 200 ``` { "items": [ { "provider_transaction_id": "521888807466", "mode": "test", "type": "collection", "channel": "momo", "subscriber_number": "0240000000", "account_number": null, "r_switch": "MTN", "description": "Invoice A104", "customer_email": "customer@example.com", "gross_amount": 10.5, "gross_amount_minor": 1050, "fee_amount": 1.58, "fee_amount_minor": 158, "net_amount": 8.92, "net_amount_minor": 892, "currency": "GHS", "status": "approved", "provider_code": "000", "provider_reason": "Transaction Successful", "created_at": "2026-07-28T12:04:22.117Z" } ], "next_cursor": "eyJ0IjoiMjAyNi0wNy0yOFQxMjowNDoyMi4xMTdaIiwiaSI6IjBmMmMtLi4uIn0", "limit": 25 } ``` ## Cursor semantics `next_cursor` is an opaque string encoding both the `created_at` and the internal id of the last row, so pagination is stable even when several transactions share the same millisecond — no row is skipped or repeated at a page boundary. Pass it back verbatim as the `cursor` query param; `null` means you are on the last page. Treat the contents as private: the encoding can change without notice, and cursors are not portable between keys or modes. Timestamp cursors issued before 2026-09-10 are still accepted for backwards compatibility, but they carry the tie-breaking bug this replaces — switch to the opaque cursor. --- # Retrieve a transaction > Fetch a single transaction by id. Fetch the live status of a single transaction by its `transaction_id`. If the transaction is still pending we poll the upstream provider and reconcile our ledger before responding; terminal transactions (approved or failed) are served straight from our ledger. > **Requires read access** Any valid key (read or read + write) can call this endpoint. ## Endpoint GET `/v1/transactions/{transaction_id}` > **ID format** `transaction_id` is the 12-digit id returned from `/v1/collect/momo` or `/v1/checkout/session`. ## Request shell ``` curl https://api.webrabbitmedia.com/v1/transactions/521888807466 \ -H "Authorization: Bearer wr_test_..." ``` ## Response Response · 200 ``` { "transaction_id": "521888807466", "code": "00", "reason_code": "approved", "reason": "Transaction Successful", "status": "approved", "resolved_status": "approved", "settled_at": "2026-09-11T11:54:31.402Z" } ``` ### Which field to trust - status and resolved_status are always the same value and both come from our ledger. Either is safe to act on. A collection only becomes approved after the upstream provider returns an explicit terminal success for that transaction — an acknowledgement of the request ("transaction initiated") never approves it. - reason_code is the machine-readable outcome and is the field to switch on: approved, pending, prompt_sent, prompt_expired, insufficient_funds, declined, cancelled, reversed, failed, upstream_error. - code is the upstream provider's numeric status code (00 success · 01 failed · 02 pending · 03 processing) or null. It is never a word — use reason_code for logic and reason for display. - settled_at is stamped only when the payment is confirmed collected, never at initiation. A pending transaction has settled_at: null. - correction is present (non-null) only on a transaction that was reported approved in error and has since been put right. Such a transaction reads status: "failed", reason_code: "settled_in_error", settled_at: null, reversed_at: null, and correction.previously_reported_status: "approved" with correction.corrected_at. The money was never collected, so this is a correction, not a reversal — if you credited on the earlier approval, unwind it. > **reason_code on transactions created before 11 September 2026** `reason_code` is recorded at settlement time from that date onward. On older transactions it was filled in afterwards from the provider's message, so it is a faithful reading of what the provider said but was not captured at the moment of settlement. ## Unknown transaction — HTTP 404 If the id does not exist for your business + mode, we return HTTP 404 — never a synthetic `failed` body. Treat 404 as "no record yet"; if you have an` Idempotency-Key ` for the request, look it up before assuming the payment did not happen. Response · 404 ``` { "error": "transaction_not_found", "transaction_id": "000000000000" } ``` ## Polling pattern Poll every ~3 seconds for up to 60 seconds. If you're still `pending` after that, keep the transaction open in your UI and continue polling at a slower cadence (e.g. every 10s) — MoMo prompts can take longer than a minute to approve. See [Webhooks](https://webrabbitmedia.com/docs/webhooks) — signed HMAC-SHA256 webhooks are live today and are the recommended path; polling is the fallback. shell ``` # poll every 3s until the transaction is no longer pending for i in $(seq 1 20); do out=$(curl -s https://api.webrabbitmedia.com/v1/transactions/521888807466 \ -H "Authorization: Bearer wr_test_...") echo "$out" echo "$out" | grep -q '"resolved_status":"pending"' || break sleep 3 done ``` --- # Me (preflight) > Verify the current key mode and business approval status before charging. Pre-flight endpoint. Returns the mode of the current key, its access scopes, and the approval status of the business it belongs to — without moving any money. Use it on deploy to detect misconfigured keys (test-in-prod, live-in-staging) and to confirm the business is cleared for live charges. > **Requires read access** Any valid key (read or read + write) can call this endpoint. ## Endpoint GET `/v1/me` ## Request shell ``` curl https://api.webrabbitmedia.com/v1/me \ -H "Authorization: Bearer wr_test_..." ``` ## Response Response · 200 ``` { "mode": "live", "business_id": "b1c8e9…", "business_name": "Acme Ghana Ltd", "business_status": "approved", "live_ready": true, "api_key_id": "9f2c…", "scopes": ["read", "write"], "commission_bps": 1000 } ``` > **Safe pre-flight check** `live_ready: false` means live charges will fail with `403`. Ship this call in your health check so you can alert before your first customer hits a dead endpoint. --- # OpenAPI & SDKs > The machine-readable OpenAPI 3.1 contract, client generation, and the TypeScript SDK. The full API is published as an OpenAPI 3.1 document. It is the contract: if this document and the prose in these guides ever disagree, the document is correct. Generate a client from it, validate your payloads against it in CI, and diff it between releases. ## Stable URLs - https://api.webrabbitmedia.com/openapi.json — served by the API edge, with ETag and a 5-minute cache. - https://api.webrabbitmedia.com/openapi.yaml — same document, YAML. - https://webrabbitmedia.com/openapi.json and https://webrabbitmedia.com/openapi.yaml — identical mirrors served with the site. Both URLs are unauthenticated, CORS-open and permanent. The document version is a date stamp in `info.version` and is echoed in the `x-openapi-version` response header. ## What it covers - Payments — health, /v1/me, mobile money collections, hosted checkout, transaction list and retrieve, and the retired payout routes. - Messaging — SMS campaigns, delivery reports, one-time passcodes, wallet balance and voice, under their own servers entry. - Webhooks — every event type, declared with OpenAPI 3.1 webhooks, one schema and one concrete example each, plus the signature headers. - Errors — a single shared error envelope with per-status examples. > **Nullability is explicit, not implied by examples** Every response schema sets `required` and nullable types. So: `fee_amount` and `net_amount` are always present numbers — on a `pending` charge `fee_amount` is `0` and `net_amount` equals `gross_amount`, because the platform fee is only assessed on approval. `account_name` is always present on a 2xx collection response, because the wallet lookup must succeed before any debit is attempted — it is absent only on the `422` `account_not_found` response. `provider_transaction_id`, `code` and `reason` are present but nullable. ## Generate a client terminal ``` npx openapi-typescript https://api.webrabbitmedia.com/openapi.json -o src/webrabbit.d.ts ``` ## Diff it in CI Pin the version you built against and fail your pipeline when it moves, so a contract change is something you read before it reaches production. ci.sh ``` PINNED=2026.09.08 LIVE=$(curl -sfI https://api.webrabbitmedia.com/openapi.json | tr -d '\r' | awk -F': ' '/^x-openapi-version/{print $2}') [ "$PINNED" = "$LIVE" ] || { echo "Web Rabbit API contract changed: $PINNED -> $LIVE"; exit 1; } ``` ## Official TypeScript SDK A thin, dependency-free client is published in the repository under `packages/webrabbit-node`. It wraps the same contract with typed methods, idempotency keys, attribution headers, a typed error class, and a webhook signature verifier. example.ts ``` import { WebRabbit } from '@webrabbit/node' const wr = new WebRabbit({ apiKey: process.env.WEBRABBIT_SECRET_KEY!, site: 'https://manyexpress.com', appName: 'Many Express', }) const charge = await wr.collectMomo( { amount: 10, subscriber_number: '0240000000', network: 'MTN', desc: 'Trip WR-1042' }, { idempotencyKey: 'trip-1042' }, ) if (charge.status === 'pending') { const latest = await wr.getTransaction(charge.transaction_id) console.log(latest.status) } ``` ## Change policy Additive changes — new optional request fields, new response fields, new webhook event types — ship without notice, so treat unknown fields and unknown event types as safe to ignore. Anything that removes or renames a field, or changes the meaning of an existing one, is announced first in the [API changelog](https://webrabbitmedia.com/docs/changelog), which is dated and separate from product release notes. ## Reading the docs without a browser Every page on this site is also published as plain Markdown, so the docs can be fetched, diffed, piped into your own tooling, or read offline with no JavaScript: - https://webrabbitmedia.com/docs/.md — one file per page (e.g. /docs/webhooks.md). - https://webrabbitmedia.com/llms.txt — index of every page. - https://webrabbitmedia.com/llms-full.txt — the entire documentation set in one file. Both `llms.txt` and `llms-full.txt` are served from the site root and from `/docs/` — `https://webrabbitmedia.com/docs/llms-full.txt` resolves to the same document. Neither path ever returns the application shell. terminal ``` curl -s https://webrabbitmedia.com/docs/webhooks.md | less ``` --- # System status > Live health of every payment route, incident updates and how to subscribe. The public status page at [/status](https://webrabbitmedia.com/status) shows the live health of every payment route by country (for example Ghana MTN Mobile Money or Nigeria bank transfer), plus the dashboard & API, hosted checkout, payouts and webhooks. Share it with your support team and customers. ## What each state means - Operational — working normally. - Degraded performance — slower than usual or some payments failing. - Partial outage — many payments on that route are failing. - Major outage — the route is down; payments cannot start. - Maintenance — planned work, announced in advance. ## How it is measured Payment routes are checked automatically every 5 minutes from real payment results and provider responses. Our team posts incidents with timestamped updates (Investigating → Identified → Monitoring → Resolved). Each route shows 90 days of daily uptime. > **During an outage** When a route can't start a payment, the API returns `503 provider_unavailable` and customers are told no money was taken. Retry later or offer another payment method. ## Subscribe to updates Enter your email at the bottom of the status page — you'll get a confirmation link first, and every update email has a one-click unsubscribe link. You can also follow the RSS feed at `/api/public/status/feed`. --- # API changelog > Dated entries for every field and behaviour change on the public API. Dated entries for every field and behaviour change on the public API. This is separate from product release notes — nothing appears here unless it can affect an integration. The matching contract is the [OpenAPI document](https://webrabbitmedia.com/docs/openapi), whose `info.version` is the date stamp shown against each entry. > **What counts as a breaking change** Removing or renaming a field, narrowing an enum, changing a status code, or changing the meaning of an existing field. Those are announced here before they ship. Adding an optional request field, an extra response field, or a new webhook event type is not breaking — ignore what you do not recognise. ## Entries 9 November 2026 Changed: disbursement fees are now 1.5% per successful transfer instead of a flat GHS 1, added on top so recipients receive the full amount. Withdrawal (payout) fees are now a flat 1.5% too — the GHS 5 minimum and GHS 100 maximum are removed. Each item's `fee` and the batch `fee_total` carry the computed amount; failed transfers are not charged. Your business may have a custom rate. Disbursements are sent through OGateway by default. 5 November 2026 Changed: card payments and `POST /v1/bills` are switched off (bills return `410`; card sessions open with Mobile Money). Added: public [status page](https://webrabbitmedia.com/docs/system-status) with incident emails and an RSS feed. 17 October 2026 Fixed: `GET /v1/checkout/session?id=` returned `not_found` because the API edge did not route GET requests. It now returns the session and its transaction. `GET /v1/checkout/session/{id}` also works. New: `reference` and `metadata` (up to 20 flat keys, 2 KB) on `/v1/checkout/session` and `/v1/collect/momo`. They are returned, together with `checkout_session_id` and `description`, in `collection.approved`, `collection.failed`, `collection.reversed` and `GET /v1/transactions/{id}`. New: `GET /v1/me` returns an `enabled` object: country, currency, collection and payout methods, Mobile Money networks, phone format and limits. New — multi-currency collection: Web Rabbit can enable extra collection countries per business. Send `country`/`currency` on `/v1/checkout/session` and `/v1/collect/momo`; networks and phone numbers are checked per country. Each currency has its own balance, payouts and payout accounts, with no conversion. Webhooks and transaction retrieve carry the real `currency`. A country that isn't enabled returns `422 currency_not_enabled` instead of silently creating a GHS payment. Test mode caps are per currency (about GHS 10 each, e.g. NGN 1,500, KES 100); amount-driven test outcomes are scaled to that cap. `GET /v1/me` lists `enabled.countries`. OGateway is now the default provider for payments and payouts for new and existing merchants, except businesses Web Rabbit has deliberately routed elsewhere. Payments already in progress finish on their original provider. No change to request or response shapes. Messaging: SMS Marketing campaigns can be sent to your own contacts or the Web Rabbit network, and every campaign can be sent as a test to your own phone first. 9 October 2026 Live payments and payouts are now open to businesses registered in any of the 16 registration countries. Each business collects and is paid out in its own country's currency; outside Ghana this runs on OGateway. (Corrected 17 October: this does not let a business in one country collect in another country's currency.) Security: API keys stop working when the teammate who created them is removed or loses edit rights; cancelling a messaging campaign through the API needs a write key in the campaign's mode; payment provider notifications are accepted only at our private callback address. 7 October 2026 OGateway is now generally available as a live rail in Ghana, alongside 360Pay. Live payments and payouts remain Ghana-only; other countries stay in test mode. 3 October 2026 OGateway: hosted checkout now offers Mobile Money in Kenya, Uganda, Tanzania, Zambia, Côte d'Ivoire, Cameroon, Gambia, Congo DRC, Benin, Burkina Faso, Mali, Senegal and Togo, and bank transfer in Nigeria (test mode outside Ghana). Failed payments now carry a plain-language `reason`. 25 September 2026 - Added Invoices: line items, discounts, taxes, due dates, email sending and reminders, part payments, manual payment records, a hosted pay page with PDF download, and the invoice.paid and invoice.partially_paid webhook events. - Added merchant products with permanent shareable links, physical, digital and service types, private product media, delivery fees and customer delivery details. - Added limited or unlimited inventory, 15-minute checkout reservations, atomic settlement, low-stock signals, reasoned stock adjustments and full-refund restocking. - Added searchable order management, fulfilment states, CSV export, and the order.paid, order.failed, order.fulfilled and inventory.low webhook events. 24 September 2026 - Added payout limits per business, by tier: a maximum per payout, per day and per month. Over-limit withdrawals are rejected with a clear error; the remaining headroom is shown in the dashboard. Bulk disbursements count toward the same daily and monthly totals. - Added a review flag on large payouts, a 24-hour pause on withdrawals after a payout account is added or changed, and a cap of 3 pending payouts at a time. - Changed payouts and disbursements are processed on working days only — Monday to Friday, excluding Ghana public holidays. Requests made on a weekend or holiday are scheduled for the next working day; payout records carry scheduled_for with the actual processing date. - No contract change. Request and response shapes are unchanged; the OpenAPI version stamp does not move. 19 September 2026 `openapi 2026.10.17` - Added Bills & Airtime: POST /v1/bills buys airtime, data bundles and utility bill payments from your settled balance. One action-based endpoint covering catalog, bundles, lookup, purchase, status and list. - Added the bill.completed and bill.failed webhook events, and the BillPurchase schema to the OpenAPI document. There is no platform fee on bills, and a failed purchase is refunded to your balance in full. 17 September 2026 - Added a payout fee of 1.5% on withdrawals, minimum GHS 5.00 and maximum GHS 100.00, deducted from the amount requested. Payout records now carry a non-zero fees, and net_amount is what reaches the destination. - Added balance_before and balance_after to payout records. Payouts created before today have neither. 14 September 2026 - Changed the standard rate for Web Rabbit Processing to a 10% starting rate. Rates may vary depending on business model, transaction volume, risk profile and payment requirements. Businesses created from today start at 10%; existing accounts keep the rate already configured for them, and settled transactions are never recalculated. - No contract change. commission_bps already carried the per-business rate — read it from GET /v1/me instead of hard-coding a percentage. Example values in the docs and the OpenAPI document now show 1000. 14 September 2026 - Corrected live-rail credentials and added environment detection. Live traffic now authenticates against the provider's live environment, and a key whose environment disagrees with its mode is reported as a mismatch on our rails health check instead of failing silently. - Confirmed that wr_test_ keys are answered end to end by the built-in simulator. No test-mode call reaches 360Pay or JuniPay, no prompt is sent to a handset and no wallet is debited. Docs that still described a provider sandbox have been corrected on the Authentication, Hosted Checkout and onboarding pages. - No contract change. Request and response shapes, status values and the OpenAPI version stamp are unchanged — nothing in an existing integration needs updating. 12 September 2026 `openapi 2026.09.12` - Added split payments: sub-accounts (/v1/sub-accounts, ledger and /v1/splits/preview) and an optional split object on /v1/collect/momo and /v1/checkout/session. Legs settle to a recipient ledger on approval and are debited back on reversal. - Added the split.settled and split.reversed webhook events, plus split_status and splits on transaction responses. - Added exact integer minor units. Every money field now has an *_minor counterpart in pesewas, and amount_minor is accepted on every request that takes an amount. Sending amount and amount_minor with different values returns amount_conflict. See Amounts & currency. - Changed next_cursor on /v1/transactions to an opaque cursor over (created_at, id), so rows sharing a timestamp are never skipped or repeated. Legacy ISO-8601 cursors are still accepted; treat the new value as opaque. - Clarified that Webrabbit-Event-Id is the canonical, retry-stable event id and equals the body id; Webrabbit-Delivery-Id differs per endpoint and must not be used for idempotency. - Documented the reversal windows (7 days Mobile Money, 120 days card) and the full per-provider raw code mapping for 360Pay and JuniPay. - Documented that a workspace can hold unlimited approved sender IDs and select one per request. 10 September 2026 `openapi 2026.09.10` - Added signed messaging delivery receipts: message.delivered, message.failed and message.rejected, one event per recipient, signed with the same HMAC-SHA256 scheme as payment events. Polling messaging/status is now a fallback, not a requirement. - Guaranteed a terminal event for every submitted message. If the upstream never reports, we close the message after 24 hours as undelivered with provider_status: "NO_DLR" and error_reason: "no_delivery_report" — messages no longer stay submitted indefinitely. - Added message to data.resource_type and the MessageReceipt payload schema to the OpenAPI document and the TypeScript SDK. 9 September 2026 `openapi 2026.09.09` - Raised rate limits. Standard is now 120 req / 10 s with a 240 burst (was 60). New Scale (600 / 10 s, burst 1,200) and Enterprise (2,000 / 10 s) tiers, granted per key without a redeploy. Status and balance reads draw on a separate polling budget. - Added x-ratelimit-reset and x-ratelimit-tier alongside the existing limit/remaining headers, on Payments and Messaging alike. - Added canonical Messaging routes on the Payments host: POST /v1/messaging/sms, GET /v1/messaging/status/{campaign_id}, POST /v1/messaging/otp, POST /v1/messaging/otp/verify, GET /v1/messaging/balance. Same auth, error envelope and x-request-id as Payments. The legacy functions/v1/messaging-* URLs keep working with no cut-off date. - Relaxed Messaging payloads: business_id and mode are now optional on API-key calls and derived from the key. Sending a value that disagrees with the key returns 403 forbidden or 400 mode_mismatch. - Added Idempotency-Key support to SMS, OTP and voice, matching Payments. - Added OTP purpose (login, payment, pin_reset, custom), plus per-call code_length and template. - Enumerated OTP verification failures — invalid_code, otp_expired, too_many_attempts, not_found — each with retryable and, where meaningful, attempts_remaining. Verifying an already-verified code returns 200 with already_verified: true instead of an error. - Corrected stale wording that described signed webhooks as planned. HMAC-SHA256 signed webhooks have been live since 5 September 2026. 8 September 2026 `openapi 2026.09.08` - Added the machine-readable contract at https://api.webrabbitmedia.com/openapi.json and /openapi.yaml, mirrored on webrabbitmedia.com. OpenAPI 3.1, covering Payments, Messaging and all webhook events. - Added the x-openapi-version response header on the spec endpoints, for pinning and CI diffs. - Documented nullability for every response field. No runtime behaviour changed: fee_amount is 0 and net_amount equals gross_amount on a pending charge; account_name is always present on a 2xx collection; provider_transaction_id, code and reason are nullable. - Published the exhaustive webhook event list — collection.approved, collection.failed, collection.reversed, payout.completed, payout.failed, sms_topup.approved — with an exact payload schema per type. - Added an official TypeScript SDK. 3 September 2026 - Changed test mode to a fully simulated rail. wr_test_ keys no longer reach 360Pay or JuniPay and never move money. - Added disclosure fields to GET /v1/me: upstream, test_mode, test_moves_real_money, test_max_amount. - Added the x-wr-upstream and x-wr-test-mode response headers. - Added a hard GHS 10 per-transaction cap in simulated test mode; larger test amounts are rejected with 400. - Added deterministic test outcomes selected by amount (approve, decline, delayed approval, insufficient funds, reversal). 28 August 2026 - Added optional attribution headers HTTP-Referer and X-Webrabbitmedia-Title so we can see which site an API key is used from. - Added the x-wr-attribution-warning response header when they are absent. Existing integrations are unaffected — undeclared traffic is still served and recorded as (undeclared). - Added 403 domain_blocked for live traffic from a site blocked after review. - Added the app object to GET /v1/me. 20 August 2026 - Changed the acceptance policy to support ride-hailing, delivery, food and booking platforms under stated conditions, where the platform is merchant of record and pays its own drivers and vendors. - Clarified that payout holds and reversals apply to fraud, laundering, serious chargeback exposure or lawful orders — not to routine review. ## Staying informed Poll `https://api.webrabbitmedia.com/openapi.json` (or just its `x-openapi-version` header) in CI and fail the build when it moves. This page is also published as Markdown at `https://webrabbitmedia.com/docs/changelog.md`, so you can diff it directly. --- # Amounts & currency > Decimal cedis and exact pesewa minor units, and the rounding rules behind them. Every money field on the API is available in two forms: a decimal amount in Ghana cedis (`amount`) and an exact integer in pesewas (`amount_minor`). They always describe the same value. Integrate against `amount_minor` if you want to keep floating point out of your ledger entirely. ## Field pairs | Field | Type | Description | | --- | --- | --- | | amount / amount_minor | number / integer | The requested or captured amount. 10.50 GHS is 1050 pesewas. | | gross_amount / gross_amount_minor | number / integer | What the customer paid. | | fee_amount / fee_amount_minor | number / integer | Web Rabbit fee deducted from the collection. | | net_amount / net_amount_minor | number / integer | What settles to you after the fee and any split legs. | | balance / balance_minor | number / integer | Sub-account balances and ledger entries. | ## Sending amounts On `POST /v1/collect/momo`, `POST /v1/checkout/session` and `POST /v1/splits/preview` you may send either field. Send `amount_minor` and nothing is ever converted through a float on our side. request ``` { "amount_minor": 1050, "subscriber_number": "0244000000", "network": "MTN" } // equivalent to { "amount": 10.50, "subscriber_number": "0244000000", "network": "MTN" } ``` > **Do not send both with different values** If you send `amount` and `amount_minor` and they disagree, the request is rejected with HTTP 400 `amount_conflict` rather than us guessing which one you meant. ## Why minor units `Math.round(value * 100)` is silently wrong for values such as `1.005`, which is stored as `1.00499999…` in IEEE 754 and rounds down. We parse decimal amounts from their exact string form, never through a float, and all fee and split arithmetic runs in integer pesewas. Remainders from percentage splits are allocated to the largest leg so the parts always sum exactly to the whole. ## Limits and rounding rules | Field | Type | Description | | --- | --- | --- | | Currency | GHS | All amounts are Ghana cedis. 1 GHS = 100 pesewas. | | Precision | 2 decimals | More than two decimal places is rejected — 10.505 is not a valid amount. | | Minimum | 0.01 GHS | 1 pesewa. Amounts of zero or below are rejected. | | Test mode cap | 10.00 GHS | Simulated test charges are capped so nothing looks like production volume. | --- # Provider codes > Lifecycle statuses and the upstream code field returned on every transaction. Every collection response includes a `code` field forwarded from the upstream provider (OGateway by default, or 360Pay or JuniPay, depending on how your business is routed), plus a normalised `status` (`approved` · `pending` · `failed`). The raw code is useful for support tickets and analytics. > **Always a string** `code` is returned as a JSON string on every endpoint, e.g. `"02"`. Keep the leading zero — never parse it as a number. › OGateway failure reasons On OGateway , the default provider, a failed payment carries a plain-language `reason` (for example insufficient balance, wrong PIN or timeout). It maps onto the same `status` as every other provider. › Several upstream providers, one status On 360Pay the raw `code` is a two-digit numeric string (`"00"`, `"02"`…). On JuniPay it is a word (`"success"`, `"pending"`, `"failed"`). Both are normalised onto the same `status`, so integrate against `status` and treat `code` as diagnostic metadata only. ## Transaction status codes The provider reports a transaction state on the settlement callback. We map it as follows: | Field | Type | Description | | --- | --- | --- | | 00 — SUCCESS | approved | Payment settled. The platform fee is applied and your net balance is credited. | | 01 — FAILED | failed | Declined, wrong PIN, insufficient funds, or cancelled by the customer. | | 02 — PENDING | pending | Charge created — the customer still has to authorise on their handset or complete checkout. | | 03 — PROCESSING | pending | Accepted by the network and being processed. Wait for the callback. | ### JuniPay codes JuniPay returns lowercase words rather than digits. Comparison is case-insensitive on our side. | Field | Type | Description | | --- | --- | --- | | success / successful / completed / paid | approved | Payment settled. | | pending / initiated / created | pending | Charge created, awaiting customer authorisation. | | processing / ongoing | pending | Accepted by the network, in flight. | | failed / declined / error | failed | Declined by the wallet, the network, or the customer. | | cancelled / canceled / abandoned | failed | Customer dismissed the prompt or the checkout expired. | | insufficient_funds | failed | Wallet balance too low. | | reversed / refunded | reversed | Settled payment clawed back — triggers collection.reversed. | | unknown code | pending | Anything we do not recognise is held as pending and re-polled — never guessed as approved or failed. | ### 360Pay codes Two-digit numeric strings. Keep the leading zero. | Field | Type | Description | | --- | --- | --- | | "00" | approved | Transaction successful. | | "01" | failed | Transaction failed — generic decline. | | "02" | pending | Awaiting customer authorisation on the handset. | | "03" | pending | Processing at the network. | | "04" | failed | Insufficient funds in the wallet. | | "05" | failed | Invalid or unregistered wallet number. | | "06" | failed | Customer cancelled or the prompt timed out. | | "07" | failed | Wallet limit or KYC restriction on the customer account. | | "09" | pending | Queued upstream; the callback resolves it. | | "12" | failed | Duplicate reference at the provider. | | "91" | pending | Network or issuer unavailable — re-polled, not failed. | | reversal callback | reversed | A settlement callback marking a prior success as reversed. | | unknown code | pending | Held as pending and re-polled. | > **Integrate against status, not code** Providers add codes without notice. `status` is our contract and is exhaustive (`pending` · `approved` · `failed` · `reversed`);`code` and `reason` are diagnostic passthrough for your support desk. ## Institution codes You send a friendly `network` value and we route it to the provider's institution code. | Field | Type | Description | | --- | --- | --- | | MTN | 300591 | MTN Mobile Money. | | TELECEL | 300594 | Telecel Cash (formerly Vodafone Cash). "VODAFONE" and "VDF" are still accepted and normalised to TELECEL. | | AT | 300592 | AT Money (AirtelTigo). "ATL", "TGO" and "AIRTELTIGO" are still accepted. | | GMONEY | 300574 | G-Money wallet. | ## Platform codes | Field | Type | Description | | --- | --- | --- | | upstream_error | failed | We could not reach the provider (HTTP 502). Safe to retry with the same Idempotency-Key. | | insufficient_scope | error | HTTP 403 — the API key is read-only. Create a key with write access. | | provider_unsupported | error | HTTP 501 — payout endpoints are retired; payouts are settled manually from the dashboard. | ## Not found Requesting a `transaction_id` that does not belong to your business (or to the current mode) returns HTTP 404 with a machine-checkable error body — never a fake `failed` verdict: ``` HTTP/2 404 { "error": "transaction_not_found", "transaction_id": "000000000000" } ``` > **Never treat 404 as a failed payment** A 404 means we have no record of that id — it does not mean the customer wasn't charged. If you created a transaction and then lost the id (network drop mid-response), recover it with [GET /v1/transactions?idempotency_key=…](https://webrabbitmedia.com/docs/idempotency) before assuming failure. › Where the code lives We surface the raw code as `code` and the human message as `reason` on every transaction response and in the ledger. Filter/search by `code` from Transactions → Payments in your dashboard. --- # Test data > Sandbox wallets and how test-mode charges behave. Use these values with a `wr_test_` key against `https://api.webrabbitmedia.com`. Test-mode requests are completely isolated from live-mode data — separate keys, separate transactions, separate balances and webhooks. > **Test mode is fully simulated — no money moves** Every test-mode call (collections, hosted checkout including card, name lookups, payouts, status checks and balances) is answered by Web Rabbit's built-in simulator. Nothing reaches OGateway, 360Pay or JuniPay, no customer receives a prompt, and no wallet is debited. Safe to run in CI. Test amounts are additionally capped at GHS 10.00 per transaction. ## Deterministic trigger amounts The amount decides the outcome, so your tests are repeatable. Any other amount up to the cap approves. | Field | Type | Description | | --- | --- | --- | | 1.00 | approved | Accepted as pending, then approved on the very next status check. | | 2.00 | declined | Fails with a customer-declined reason. | | 3.00 | pending → approved | Stays pending for about 15 seconds, then approves. | | 4.00 | pending → failed | Stays pending for about 15 seconds, then fails. | | 5.00 | insufficient_funds | Fails with an insufficient-funds reason. | | 6.00 | approved → reversed | Approves, then reverses about 60 seconds later so you can test reversal webhooks. | | Above 10.00 | 422 test_amount_above_limit | Rejected before anything is created. Use live mode for larger amounts. | > **Collections are always accepted as pending** Exactly like the real rails: name verification runs first, the collection is accepted as `pending`, and the outcome arrives on the status check and the webhook. For the terminal triggers (1, 2, 5) the outcome is already final on the first poll. ## Test wallets Accepted phone formats: local `0240000000` or international `233240000000`. Networks: `MTN`, `TELECEL`, `AT`, `GMONEY`. | Field | Type | Description | | --- | --- | --- | | Any valid wallet | MTN / TELECEL / AT / GMONEY | Resolves to a stable simulated account name. No prompt is sent to the handset. | | Numbers ending 0000 | 422 account_not_found | Simulated lookup failure, so you can test the rejection path. | | Any account number | BANK | Bank name verification is simulated too, with the same 0000 failure rule. | ## Knowing which rail you are on `GET /v1/me` discloses the routed provider and the exact test-mode behaviour, so integrations can guard themselves. The same values are returned on every API call as the `x-wr-upstream` and `x-wr-test-mode` response headers. GET /v1/me ``` { "mode": "test", "upstream": "simulator", "test_mode": "simulated", "test_moves_real_money": false, "test_max_amount": 10 } ``` `test_moves_real_money` is `false` for every account by default. It only becomes `true` if you explicitly ask Web Rabbit support to point your test mode at live rails (rare, for provider-side certification); the GHS 10 cap still applies. Always read this field before running automated collection tests. ## Hosted Checkout in test mode `POST /v1/checkout/session` returns a real branded `/pay/` checkout URL that expires after 15 minutes. In test mode both Mobile Money and card are simulated — the card option settles on our page instead of redirecting to the provider's hosted form, and follows the same trigger amounts. Confirm the outcome with `GET /v1/transactions/{id}`. ## Example test charge shell ``` curl -X POST https://api.webrabbitmedia.com/v1/collect/momo \ -H "Authorization: Bearer wr_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: test-$(date +%s)" \ -d '{ "amount": 1.00, "subscriber_number": "0240000000", "network": "MTN", "desc": "Smoke test" }' ``` --- # Webhooks > Signed, retried event delivery for collections, payouts and top-ups. Mobile Money and Hosted Checkout payments are asynchronous: a charge starts as `pending` while the customer authorises it. When the payment reaches a final state, Web Rabbit sends an HMAC-signed `POST` to every endpoint you have registered — so you never have to poll. > **data is the envelope — data.object is the transaction** Read the transaction from `data.object`, not from `data`. `data` also carries `resource_type` and `resource_id` beside the object. This is deliberately different from `GET /v1/transactions/{id}`, which returns the transaction flat at the top level. A handler written against the flat shape parses nothing and fails silently. > **One webhook, any gateway** Events are emitted by our settlement layer, not by the underlying provider. Whether a charge was routed through OGateway, 360Pay or JuniPay, you receive the same signed payload with the same fields. ## Register an endpoint In the merchant dashboard go to Developer → Webhooks , add an `https://` URL, and choose the events you want. Endpoints are scoped per mode: test endpoints only receive test events and live endpoints only receive live events. You can register up to 5 endpoints per mode, send a test event at any time, and rotate the signing secret without downtime. The signing secret is shown once, at creation and after each rotation. ## Events | Field | Type | Description | | --- | --- | --- | | collection.approved | transaction | A MoMo or checkout collection was approved and credited to your balance. | | collection.failed | transaction | A collection failed, was declined, or expired. Also used when a collection reported approved in error is put right — that payload carries a correction block and reason_code settled_in_error. | | collection.reversed | transaction | A previously approved collection was clawed back by the provider after real money moved. The amount and our commission are removed from your balance. Never used for a payment that was never collected. | | split.settled | transaction | A collection with a split instruction settled — every leg was credited to its sub-account ledger. | | split.reversed | transaction | A settled split collection was reversed — every credited leg was debited back. | | payout.completed | payout | A withdrawal was disbursed to your bank or wallet. gross_amount is the amount requested, fees is the payout fee, and net_amount is what reached the destination. | | payout.failed | payout | A withdrawal failed; the full amount requested, including the payout fee, is returned to your available funds. | | sms_topup.approved | sms_topup | A messaging wallet top-up was credited. | | message.delivered | message | One SMS reached the handset. Emitted per recipient, not per campaign. | | message.failed | message | One SMS could not be delivered, or no delivery report arrived within 24 hours (status undelivered, provider_status NO_DLR, error_reason no_delivery_report). | | message.rejected | message | The provider or network refused the message outright — sender ID, blocked number, or content. | ### Delivery receipts Messaging emits one terminal event per message, signed with the same scheme as payment events, so you never have to poll `messaging/status` in a loop. Every submitted message settles exactly once: whichever arrives first — the provider callback, a status poll, or our sweeper — is the one that fires the event. Messages with no upstream report are closed as `undelivered` after 24 hours, so nothing stays pending indefinitely. Test mode emits the same events deterministically without touching the provider. Payment provider notifications are accepted only at our private callback address; anything else is refused. If a notification never arrives, the payment still settles through our automatic status check, which runs every 10 minutes. message.delivered ``` { "id": "0f61b6a5-3c58-4d0f-9d8f-8b1f1a2c7de1", "type": "message.delivered", "mode": "live", "created_at": "2026-09-10T09:12:44.108Z", "data": { "resource_type": "message", "resource_id": "a3f1c0de-6b21-4d38-9c1a-2f70b8d51e44", "object": { "message_id": "a3f1c0de-6b21-4d38-9c1a-2f70b8d51e44", "campaign_id": "c14b7c2e-9a4d-4c07-9e12-0f3b6f2a8d55", "to": "+233240000000", "sender": "WRB", "mode": "live", "status": "delivered", "provider_status": "DELIVERED", "provider_message_id": "SM-88213004", "error_reason": null, "segments": 1, "cost": 0.045, "submitted_at": "2026-09-10T09:12:41.002Z", "settled_at": "2026-09-10T09:12:44.061Z" } } } ``` ## Payload The body is JSON. `data.object` for a collection is the same object returned by `GET /v1/transactions/{transaction_id}`. collection.approved ``` { "id": "b6f1c1de-6a1f-4b9e-9b0e-7f0f5a2d1c33", "type": "collection.approved", "mode": "live", "created_at": "2026-08-22T10:14:05.221Z", "data": { "resource_type": "transaction", "resource_id": "521888807466", "object": { "transaction_id": "521888807466", "provider_transaction_id": "JP-88213771", "status": "approved", "resolved_status": "approved", "code": "00", "reason_code": "approved", "reason": "TRANSACTION SUCCESSFUL", "subscriber_number": "0240000000", "channel": "momo", "gross_amount": 10, "fee_amount": 1.5, "net_amount": 8.5, "currency": "GHS", "created_at": "2026-08-22T10:13:41.005Z" } } } ``` ## Headers | Field | Type | Description | | --- | --- | --- | | Webrabbit-Signature required | string | t= ,v1= | | Webrabbit-Event-Id required | string | Canonical event id — identical to the body's id and identical on every retry. Key your idempotency on this. | | Webrabbit-Event-Type required | string | e.g. collection.approved | | Webrabbit-Delivery-Id required | string | Unique per delivery attempt chain. | | Webrabbit-Attempt required | integer | Attempt number, starting at 1. | ### Which id is canonical The `Webrabbit-Event-Id` header is authoritative. It is the same value as the `id` in the untransformed body, it is generated once when the event is created, and it never changes across retries or across multiple endpoints receiving the same event. The `Webrabbit-Delivery-Id` identifies the attempt chain to a single endpoint and will differ per endpoint — never key idempotency on it. If you use a payload transformation, the body may be reshaped, but the header is untouched, which is the other reason to prefer it. ### Reversal window `collection.reversed` can arrive well after settlement. Mobile Money collections can be clawed back by the network for up to 7 days after settlement; card collections carry chargeback exposure for up to 120 days . If you pay out to drivers, vendors or couriers faster than that window, size your payout hold or reserve accordingly — you are carrying the reversal risk for anything already disbursed. ### Corrections — approved reported in error If a collection was ever reported `approved` without the money actually being collected, we put it right with a `collection.failed` event carrying a `correction` block, not a `collection.reversed`. Nothing was collected, so there is nothing to claw back — but if you credited a customer on the earlier approval, this is your signal to unwind it. The transaction ends up `status: "failed"`, `reason_code: "settled_in_error"`, `settled_at: null`, `reversed_at: null`, and any platform fee is removed. collection.failed · correction ``` { "type": "collection.failed", "data": { "resource_type": "transaction", "resource_id": "912751664555", "object": { "transaction_id": "912751664555", "status": "failed", "reason_code": "settled_in_error", "reason": "Reported approved in error from a provider acknowledgement; the payment was never collected", "settled_at": null, "reversed_at": null, "fee_amount": 0, "correction": { "previously_reported_status": "approved", "corrected_at": "2026-09-11T14:02:11.000Z", "detail": "If you credited on the earlier approval, unwind it." } } } } ``` ## Verify the signature Compute `HMAC-SHA256` over ``${t}.${rawBody}`` using your signing secret and compare it to `v1` with a constant-time comparison. Always use the raw request body — re-serialising the JSON changes the bytes and breaks the signature. Reject timestamps older than five minutes to prevent replays. verify.js ``` import crypto from 'node:crypto' // Express: app.post('/webhooks/webrabbit', express.raw({ type: '*/*' }), handler) export function handler(req, res) { const header = req.get('Webrabbit-Signature') || '' const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))) const raw = req.body // Buffer — never req.body parsed as JSON const expected = crypto .createHmac('sha256', process.env.WEBRABBIT_WEBHOOK_SECRET) .update(`${parts.t}.${raw}`) .digest('hex') const ok = parts.v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)) && Math.abs(Date.now() / 1000 - Number(parts.t)) < 300 if (!ok) return res.status(400).send('invalid signature') const event = JSON.parse(raw.toString()) // ... fulfil the order, then acknowledge fast res.status(200).send('ok') } ``` ## Retries and failures Reply with any `2xx` within 10 seconds to acknowledge. Anything else — a non-2xx status, a timeout, or a connection error — is retried up to 6 attempts with backoff at `10s → 1m → 5m → 30m → 2h → 6h`. After 10 consecutive events exhaust their retries, the endpoint is disabled automatically and you can re-enable it from the dashboard once the issue is fixed. Every attempt, its response code and body are visible under Developer → Webhooks → Deliveries , where you can also replay a delivery manually. > **Acknowledge first, process later** Do the slow work (emails, fulfilment, third-party calls) after you have returned `200`. Events can be delivered more than once, so make your handler idempotent — key it on `Webrabbit-Event-Id` or the `transaction_id`. ## Testing an endpoint Open an endpoint and use the Testing tab to dispatch a realistic example of any event type you are subscribed to. The example is signed with your real signing secret and appears in Message attempts alongside production traffic, so you can confirm your signature check, headers and parsing before a real payment happens. Example events carry `"mode": "test"` and are never counted as settlement activity. ## Message attempts and replay Message attempts lists every delivery with its status, response code, message ID, duration and full request body. Filter by status or event type, expand a row to inspect what we sent and what your server returned, and press Resend to re-queue a single message. The Replay menu handles bulk recovery over a time window: | Field | Type | Description | | --- | --- | --- | | Recover failed messages | bulk | Re-queues every delivery in the window that exhausted its retries. Use this after fixing an outage. | | Replay missing messages | bulk | Sends subscribed events from the window that never reached this endpoint — for example while it was disabled. | | Bulk replay | bulk | Re-sends every subscribed event in the window, including ones that already succeeded. Your handler must be idempotent. | Each run queues at most 200 messages and still respects your endpoint throttle, so a large replay drains gradually rather than flooding your server. ## Advanced delivery options The Advanced tab on an endpoint holds three optional controls. ### Custom headers Add key/value headers sent with every request to this endpoint — a shared secret for a gateway, a tenant identifier, or a routing hint. Reserved headers (`Webrabbit-*`, `Content-Type`, `Host`) cannot be overridden. ### Endpoint throttling Cap how many events per minute we deliver to this endpoint. Anything above the cap waits in the queue and is delivered on the next run instead of being dropped — useful for rate-limited or shared hosting. ### Payload transformation Rewrite the request before it leaves us, so a legacy consumer does not need to change. Write a synchronous `handler(webhook)` function that returns any of `payload`, `url` or `method`. It runs in an isolated sandbox: no network, no timers, no `async`/`await`, an 8 KB source limit, a hard execution timeout and a 256 KB cap on the transformed body. The signature is always computed over the final body, so verification works unchanged. transformation.js ``` function handler(webhook) { // webhook.payload — the Web Rabbit event body // webhook.url — the current destination // webhook.method — "POST" const tx = webhook.payload.data.object return { payload: { kind: webhook.payload.type, reference: tx.transaction_id, amount_minor: Math.round(tx.gross_amount * 100), currency: tx.currency, }, } } ``` Use Simulate in the same tab to run your function against a sample payload and see the exact body we would send. If a transformation throws at delivery time we fall back to the original payload and record the error on the attempt, so a bad transform never silently drops an event. ## Polling fallback Webhooks are the recommended path, but polling still works. After `POST /v1/collect/momo` returns `202`, poll `GET /v1/transactions/{transaction_id}` every 3 seconds up to 20 times (~60 seconds — the standard MoMo prompt window). shell ``` id=521888807466 for i in $(seq 1 20); do tx=$(curl -s https://api.webrabbitmedia.com/v1/transactions/$id \ -H "Authorization: Bearer wr_live_...") echo "$tx" echo "$tx" | grep -q '"resolved_status":"pending"' || break sleep 3 done ``` --- # Messaging overview > Base URL, messaging keys, credits and error shapes for SMS, OTP and voice. The Messaging API sends bulk SMS and one-time passcodes to Ghanaian numbers. Outbound voice calling is not currently offered. It is billed from your messaging credit wallet , which is separate from your payments balance. ## Base URL Base URL ``` https://api.webrabbitmedia.com/v1/messaging ``` Messaging now shares the Payments host, bearer auth, error envelope, `x-request-id` and rate-limit headers. Sends are `POST` with JSON; delivery status and balance are `GET`. | Field | Type | Description | | --- | --- | --- | | POST /v1/messaging/sms | send | Bulk SMS campaign. | | GET /v1/messaging/status/{campaign_id} | read | Delivery report. Higher polling budget. | | POST /v1/messaging/otp | send | Issue a one-time passcode (or verify with action: "verify"). | | POST /v1/messaging/otp/verify | verify | Check a code. Free. | | GET /v1/messaging/balance | read | Messaging wallet balance. | › The old URLs still work Everything built against `https://webrabbitmedia.com/functions/v1/messaging-*` keeps working exactly as before, with no cut-off date. The canonical routes above are what new integrations should use. ## Workspace and mode come from your key A messaging API key is issued for one workspace and one mode, so `business_id` and `mode` are optional in the body — we read them off the key. Send them anyway if you prefer explicit payloads; if what you send disagrees with the key we reject the call with `403 forbidden` (wrong workspace) or `400 mode_mismatch` rather than guessing. Dashboard sessions still pass both. ## Idempotency SMS, OTP and voice accept the same `Idempotency-Key` header as Payments. A retry with the same key and body returns the original result instead of sending again; the same key with a different body returns `409 idempotency_conflict`. See [Idempotency](https://webrabbitmedia.com/docs/idempotency). ## Authentication Create a messaging key under Messaging → Developer → API Keys . Messaging keys are scoped to the messaging product — a payments key returns `403 insufficient_scope`, and the reverse is also true. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/messaging/sms \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "business_id": "…", "mode": "live", "sender": "WEBRABBIT", "recipients": ["0240000000"], "message": "Hello" }' ``` > **Write access required for sends** Sending SMS, OTPs or voice calls needs a `write` key. Read-only keys can still fetch delivery status and balances. ## Test mode & live mode Pass `mode` as `test` or `live`. Test mode never touches the network: campaigns are recorded, the wallet is debited from your test wallet, and messages settle as `delivered` immediately with `simulated: true` in the response. ## Credits & refunds Cost is calculated before the send and debited up front, so an accepted send can never be free. If the provider rejects the batch, the campaign is marked `failed` and the full amount is refunded to your wallet ledger automatically. Cancelling a queued or scheduled campaign refunds its credits. Because that moves money, cancelling through the API needs a write key in the same mode (live or test) as the campaign; read-only keys get `403 insufficient_scope`. | Field | Type | Description | | --- | --- | --- | | SMS | per segment | 160 GSM characters per segment; 70 for Unicode. Cost = segments × recipients × rate. | | OTP | per message | Billed as a single-segment SMS. Verification is free. | ## Errors Response · 402 ``` { "error": "insufficient_credits", "message": "Not enough messaging credits. Top up your wallet." } ``` | Field | Type | Description | | --- | --- | --- | | 400 invalid_request | error | Bad sender ID, empty message, or no valid recipients. | | 401 unauthorized | error | Missing or revoked key. | | 403 forbidden | error | Key is not scoped to this workspace, product, mode or access level. | | 402 insufficient_credits | error | Wallet balance is lower than the calculated cost. | | 409 idempotency_conflict | error | Idempotency-Key reused with a different body, or the first call is still in flight. | | 429 rate_limited | error | Same limits and headers as Payments. See Rate limits. | | 502 provider_error | error | Upstream network rejected the batch. Credits are refunded. | ## SMS Marketing From the dashboard (Messaging → Marketing) you can run campaigns without writing code: - Your contacts — import consented contacts, build audiences and personalise with first name or city. - Web Rabbit network — reach people who have paid Web Rabbit merchants, filtered by business category and city. Numbers are always masked, every campaign is reviewed before it sends, each person gets at most 2 offers a week, and every message ends with a STOP opt-out line. - Packages — SMS bundles for your contacts and reach packages for the network. Bundle SMS is used before wallet balance. - Templates — save messages for your contacts, the network, or both. Network messages cannot use personal fields. > **Send a test to yourself first** Every campaign page has a "Send test" button that delivers the exact final message to your own phone, up to 5 tests a day, before you launch. --- # Send SMS > Create a bulk SMS campaign and pull per-recipient delivery reports. Send a bulk SMS campaign. One call creates the campaign, expands your recipient list, debits the wallet and hands the batch to the network. ## Endpoint POST `/v1/messaging/sms` > **No sender ID yet? Use WRB** `sender` is 3–11 letters or digits. If you have not registered your own sender ID, send with `WRB` — the shared Web Rabbit sender name, available to every workspace from day one. For branded messages register your own under Messaging → Bulk SMS → Sender IDs ; once it is approved it becomes your default. Unregistered names are rejected by the networks with “Sender Id Not Allowed”. ## Request | Field | Type | Description | | --- | --- | --- | | business_id | string | Optional on API-key calls — taken from the key. Required for dashboard sessions. A value that disagrees with the key is rejected. | | mode | enum | test or live. Optional on API-key calls — taken from the key. | | sender required | string | Approved sender ID, 3–11 alphanumeric characters. Defaults to the shared WRB name. | | message required | string | Message body. Max 1600 characters. | | recipients | string[] | Numbers in local (0240000000) or international (233240000000) format. | | group_ids | string[] | Contact group ids. Merged with recipients; opted-out contacts are skipped. | | name | string | Campaign name shown in the dashboard. | | schedule_at | string | ISO timestamp. Omit to send immediately. The campaign is handed to the network scheduler when it accepts it, otherwise Web Rabbit holds it and releases it at the requested time. The response says which, in scheduled_by. | | timezone_id | string | Network time-zone id used with schedule_at. Ids come from GET /v1/messaging/reference?kind=timezone. | | validity_seconds | integer | How long the network keeps retrying an unreachable handset before giving up. 60 to 259200 seconds. | | dlt_entity_id | string | Regulator entity id, for markets that require registration (for example India DLT). | | dlt_template_id | string | Regulator template id. | | tlv | object[] | Regulator TLV tags, each { tag, tagValue }, passed straight through to the network. | Recipients are de-duplicated and validated. A campaign accepts at most 10,000 numbers. > **Safe retries** Send an `Idempotency-Key` header and a retry of the same body returns the original campaign instead of sending twice. Reusing a key with a different body returns `409 idempotency_conflict`. Same rules as Payments. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/messaging/sms \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "business_id": "b0a1…", "mode": "live", "name": "October promo", "sender": "WRB", "message": "Our October sale is live — 20% off everything.", "recipients": ["0240000000", "233201112233"] }' ``` ## Response Response · 200 ``` { "ok": true, "campaign_id": "3f0c…", "provider_campaign_id": "664a…", "cost": 4.40, "recipients": 12, "breakdown": [ { "country": "Ghana", "iso2": "GH", "recipients": 10, "rate_per_segment": 0.20, "cost": 2.00 }, { "country": "Nigeria", "iso2": "NG", "recipients": 2, "rate_per_segment": 0.20, "cost": 0.40 } ], "credit_left": 4180 } ``` | Field | Type | Description | | --- | --- | --- | | campaign_id | string | Web Rabbit campaign id. Use it to pull delivery reports. | | provider_campaign_id | string | Upstream batch id, null while queued. | | cost | number | Credits debited: the sum of segments × rate for each recipient, priced by destination country. | | breakdown | array | Cost grouped by destination country: recipients, rate per segment and subtotal. | | recipients | number | Valid, de-duplicated numbers accepted. | | rejected | number | Recipients the network refused. Their credits are refunded at exactly what they cost. | | simulated | boolean | Present and true for test-mode sends. | ## What a message costs Messages are paid from your messaging wallet. SMS from a bundle package is used before wallet balance, and Web Rabbit can also add credit to your wallet directly. Price is per segment and depends on the country the recipient's number belongs to, because that is how the networks charge us. Every destination is priced on that country's local network rate: we take the carrier price in euro, convert it to cedis and apply our margin: Formula ``` price per segment = ceil(carrier EUR rate × EUR→GHS × 3) , minimum GHS 0.20 campaign cost = Σ segments × price per segment (per recipient) ``` | Field | Type | Description | | --- | --- | --- | | Ghana (+233) | GHS 0.20 | Per 160-character segment. The floor price. | | Nigeria (+234) | GHS 0.20 | Local network rate. | | Kenya (+254) | GHS 1.35 | Local network rate. | | South Africa (+27) | GHS 0.36 | Local network rate. | | Senegal (+221) | GHS 0.76 | Local network rate. | | Anywhere else | GHS 3.96 | Fallback for destinations outside the published list. | The full list of 52 African destinations, and the exact price we will charge you, is shown live under Messaging → Wallet → Price per country . Prices move with the carrier sheet and the exchange rate; the `breakdown` in every send response tells you precisely what you were charged and why. Wallets stay in GHS whatever the destination. Numbers may be written as Ghana local (`0240000000`) or in full international form (`233240000000`, `2348030000000`). A local number is always read as Ghana. ## Multiple sender IDs A workspace can hold as many approved sender IDs as it needs — one per product line, for example `RIDES`, `DELIVERY` and `SAFETY`. There is no limit and no extra cost; each name is registered and approved separately with the networks, which typically takes 1–3 business days. Register them under Messaging → Sender IDs . Sender selection is per request: set `sender` to any approved name on your workspace for that mode. Sending an unapproved name is rejected with HTTP 400 before the message is charged, and `WRB` is always available as a fallback while a new name is in review. When you request a name we ask for three things: the message type — transactional, promotional, or both — the country the name is registered for (Ghana by default), and a short use case with a sample message. The networks only accept one of those three message types, so it is a choice rather than free text; your use case and sample travel with the request as the supporting detail. If a request is saved but the network has not picked it up yet, use Resend to network on the row instead of creating the name again. ## Delivery reports > **Use webhooks, not polling** Subscribe to `message.delivered`, `message.failed` and `message.rejected` and we push one signed receipt per recipient, with the same HMAC-SHA256 scheme as payment events. Every submitted message gets exactly one terminal event; if the network never reports, we close it after 24 hours as `undelivered`. The endpoint below is a fallback for forcing a refresh. POST `/messaging-status` Poll for per-recipient outcomes. Each call reconciles the campaign’s messages with the network’s report and returns how many rows changed. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/messaging/messaging-status \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "3f0c…" }' ``` Response · 200 ``` { "ok": true, "updated": 12 } ``` Message statuses move `queued → submitted → delivered`, or to `undelivered`, `failed` or `rejected`. Unknown network codes stay `submitted` until the next report, and at most 24 hours — after that we settle the message as `undelivered` with `provider_status: "NO_DLR"` and emit `message.failed`. --- # OTP > Send and verify one-time passcodes with hashed storage and expiry. Send and verify one-time passcodes. We generate the code, hash it before storage, deliver it over SMS and expire it for you — you never handle the plaintext code. ## Send a code POST `/v1/messaging/otp` | Field | Type | Description | | --- | --- | --- | | action | enum | send (default) or verify. | | business_id | string | Optional on API-key calls — taken from the key. | | mode | enum | test or live. Optional on API-key calls — taken from the key. | | phone required | string | Ghanaian number, local or international format. | | purpose | enum | login, payment, pin_reset or custom. Picks the saved preset for that kind of code and is recorded on the request. | | code_length | integer | 4–8 digits for this call only. | | template | string | Wording for this call only. Must contain {code}; {minutes} is replaced with the expiry. | > **Per-call options win, then the purpose preset, then your settings** Configure them on the OTP page. Defaults: 6 digits, 5 minute expiry, template `Your verification code is {code}. It expires in {minutes} minutes.` A sender ID must be set before OTPs can be sent. shell ``` curl -X POST https://api.webrabbitmedia.com/v1/messaging/otp \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{ "action": "send", "business_id": "b0a1…", "mode": "live", "phone": "0240000000" }' ``` Response · 200 ``` { "ok": true, "request_id": "9c71…", "expires_at": "2026-08-12T14:35:00.000Z", "cost": 0.035 } ``` ## Verify a code Send `action: "verify"` with the `request_id` from the send call and the code the user entered. Verification is free. There is also a dedicated route, `POST /v1/messaging/otp/verify`, which takes just `request_id` and `code`. Verifying a code twice is safe: the second call returns `200` with `already_verified: true`. Response · 200 ``` { "ok": true, "status": "verified" } ``` Response · 400 — every failure is enumerated ``` { "error": "invalid_code", "message": "That code is not correct", "retryable": true, "attempts_remaining": 3, "request_id": "b3f1…" } ``` | Field | Type | Description | | --- | --- | --- | | 400 invalid_code | error | Wrong code. retryable: true, with attempts_remaining. The attempt counter increments. | | 400 otp_expired | error | The code passed its expiry window. retryable: false — send a new one. | | 429 too_many_attempts | error | Five failed attempts on one request id. retryable: false — issue a new code. | | 404 not_found | error | Unknown request_id. retryable: false. | › Test mode In test mode no SMS leaves the platform, but the request row, expiry and verification flow behave exactly as they do live so you can build against the real contract. --- # WhatsApp, opt-outs & lookup > WhatsApp templates, the do-not-contact list, number lookup and phone verification. Beyond SMS, OTP and voice, the messaging API covers WhatsApp template campaigns, a do-not-contact list, network number lookup and phone verification. All four are priced per destination and charged from the same messaging credit balance, and all four are simulated in test mode. ## Opt-outs POST `/v1/messaging/opt-outs` Numbers on this list are removed from a send before it is priced, so you are never charged for them. Additions are mirrored to the network's own do-not-contact list. | Field | Type | Description | | --- | --- | --- | | action | enum | list (default), add or remove. | | business_id | string | Optional on API-key calls — taken from the key. | | msisdns | string[] | Numbers to add or remove. msisdn accepts a single number. | | source | string | Where the opt-out came from, e.g. dashboard, stop_reply, support. | bash ``` curl -X POST https://api.webrabbitmedia.com/v1/messaging/opt-outs \ -H "Authorization: Bearer wr_live_..." \ -H "Content-Type: application/json" \ -d '{"action":"add","msisdns":["0240000000"],"source":"stop_reply"}' ``` A send whose whole recipient list has opted out fails with `all_recipients_opted_out`. Otherwise the send response reports `skipped_opt_out` alongside the recipients that were charged. ## Number lookup POST `/v1/messaging/lookup` Ask the network whether a line is live and which operator holds it, before paying to message a large list. A result is reused free of charge for 30 days; failed checks are refunded automatically. | Field | Type | Description | | --- | --- | --- | | action | enum | lookup (default) or history. | | msisdns required | string[] | Up to 1,000 numbers per request. | | refresh | boolean | Ignore the 30-day cache and re-check every number. | json ``` { "ok": true, "charged": 0.30, "cached": 0, "results": [ { "msisdn": "0240000000", "live": true, "operator": "MTN Ghana", "country_iso": "GH", "ported": false, "cost": 0.30 } ], "credit_left": 41.70 } ``` ## WhatsApp POST `/v1/messaging/whatsapp` WhatsApp requires a template approved by Meta. Pull your approved templates, then send one as a campaign. Delivery reports arrive on the same signed `message.delivered` / `message.failed` webhooks as SMS. | Field | Type | Description | | --- | --- | --- | | action | enum | templates, sync_templates, template, create_template, send, otp, campaigns or status. | | template | object | For create_template: name, body_text, category_id, language, header_text, footer_text, variables, button fields and an optional media header. Submitted to WhatsApp for approval. | | template_id | string | Approved template reference. Required for send and otp. | | recipients | string[] | Recipient numbers. group_ids adds saved contact groups. | | sample | string | Comma-separated values for the template variables, in order. | | schedule_at | string | ISO timestamp. The network holds the campaign until then. | | phone / code | string | For action=otp: the recipient and the code to deliver. | > **Templates must be approved first** Only templates whose status is approved can be sent. Use `sync_templates` to pull the current list and approval state from the network. ## Phone verification POST `/v1/messaging/verify` Three methods, all with the same two-step shape. `network` lets the operator generate, send and check the code. `flash` places a missed call whose caller-ID digits are the code. `whatsapp` delivers the code on WhatsApp. For codes you want us to generate and send over SMS, use the OTP endpoint instead. | Field | Type | Description | | --- | --- | --- | | action | enum | send (default), check or list. | | method | enum | network, flash or whatsapp. | | phone required | string | The number being verified. | | caller | string | Required for flash: the number the missed call comes from. | | template_id | string | Required for whatsapp. | | verification_id / code | string | For action=check. | Codes expire after 10 minutes and allow five attempts. In test mode nothing is sent and `123456` is always accepted. ## Contacts POST `/v1/messaging/contacts` Your own address book. Contacts belong to your business alone — they are never written to, or read back from, a shared network address book, so no other business can see or import them. | Field | Type | Description | | --- | --- | --- | | action required | enum | list, create, import, update or delete. | | phone | string | Local (0240000000) or international format. | | first_name / last_name / email | string | Optional contact details. | | tag | string | Free-text label used to segment your audience. | | group_id | string | Add the contact to a saved group. | | contacts | object[] | For import: a list of { phone, first_name, last_name, email, tag }. | | contact_id | string | Required for update and delete. | ## Reference data GET `/v1/messaging/reference` The network's own country, time-zone and currency ids. Pass `?kind=country`, `timezone` or `currency`, or omit it for every list. Time-zone ids are what `timezone_id` expects on a scheduled send. bash ``` curl "https://api.webrabbitmedia.com/v1/messaging/reference?kind=timezone" \ -H "Authorization: Bearer $WEBRABBIT_MESSAGING_KEY" ``` --- # Voice & balances > Place outbound voice campaigns, pull call reports and read network balances. Place outbound calls to your customers: a spoken text-to-speech message, a recorded audio file, a spoken one-time code, or a bridge that connects two numbers. Calls are charged from the same messaging wallet as SMS and refunded for every call the network refuses. > **Test mode never dials** With a test-mode key, calls are simulated end to end: nothing reaches the network, the wallet moves test credits only, and each call settles as `completed`. ## Endpoint POST `/v1/messaging/voice` | Field | Type | Description | | --- | --- | --- | | action | string | speech (default), audio, otp or bridge. | | name | string | Campaign name shown in the dashboard. | | caller_id | string | Number or approved name shown to the recipient. | | script | string | What the voice reads out. Required for speech. | | audio_url | string | Public https link to the recording. Required for audio. | | otp_text | string | The code to read out. Required for otp. | | recipients required | string[] | Up to 5,000 numbers per campaign. | | bridge_to | string | Second leg of a bridge call, used with to. | | schedule_at | string | ISO time to release the campaign later. | Text-to-speech broadcast ``` curl -X POST https://api.webrabbitmedia.com/v1/messaging/voice \ -H "Authorization: Bearer $WEBRABBIT_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Delivery reminder", "caller_id": "0240000000", "script": "Hello, your order is on its way.", "recipients": ["0240000000", "0240000001"] }' ``` Response · 200 ``` { "ok": true, "campaign_id": "b1f2…", "recipients": 2, "submitted": 2, "rejected": 0, "cost": 4.76, "breakdown": [ { "country": "Ghana", "iso2": "GH", "recipients": 2, "rate_per_call": 2.38, "cost": 4.76 } ] } ``` ## Call statuses Each call moves through `queued` → `submitted` → one of `answered`, `completed`, `no_answer`, `busy`, `rejected`, `failed` or `cancelled`. The network reports the outcome to us as it happens, so the dashboard updates without a refresh. ## Pricing Voice is priced per call by destination country, the same way [SMS](https://webrabbitmedia.com/docs/messaging-sms) is. Calls the network rejects are credited straight back to your wallet. --- # Merchant Acceptance Policy > Businesses we support, the prohibited list, review categories, and enforcement. Web Rabbit Payments is a payment service provider for businesses in Ghana — digital products and online services, and platforms that collect payment online for services fulfilled in the real world, such as rides, deliveries, food orders and bookings. We are responsible to our banking, mobile money and compliance partners for what is processed through our rails, so we maintain clear standards for the businesses we onboard. This policy explains what we support, what we cannot support, and how we enforce it. ## Businesses we support - SaaS & AI products - Digital goods and downloadable products - Courses and learning material - Templates, plugins and apps - Platforms and on-demand services — ride-hailing, courier and delivery, food ordering and bookings (see the conditions below) We assess value by what a customer actually receives after paying. Products that do what they promise, are genuinely useful, and feel worth the price sail through. Offerings built on hype, vague claims, artificial urgency or repackaged free content get a closer look and may be declined. ## What we look for - Legally compliant — complies with consumer protection, data privacy and local law in every region you sell to. - Honest and helpful — real value for users, no deceptive or exploitative mechanics. - Deliverable — digital delivery that happens immediately and reliably after payment. ## Prohibited businesses > **Not an exhaustive list** We may place an account under review or suspend it immediately if we determine the business model is deceptive, harmful, high-risk, generates excessive refunds or chargebacks, or has been flagged by one of our payment partners. - NSFW, intimacy & adult content or services — Explicit or suggestive content, whether real or AI-generated — adult subscription platforms, NSFW chatbots, webcam streams, erotic games, adult toys, escort-style services. - Manual digital services — Custom design, development, coaching, freelancing or consulting sold online. If most of the value sits in human labour rather than a digital system, we cannot support it. - Digital products with limited or unclear value — Thin AI-generated PDFs, heavily marked-up templates, unfinished sites sold at premium prices, waitlists, teaser pages or "coming soon" products with no immediate usable value. - Shipped physical merchandise & direct in-person services — Merchandise, hardware, books and supplements that have to be shipped, and one-to-one in-person services sold directly by the provider (home visits, salon or repair bookings taken on your own site). On-demand services booked and paid for through a licensed platform — rides, deliveries, food orders — are supported; see "Platforms, marketplaces and on-demand services" below. - Illegal or age-restricted goods — Drugs, alcohol, tobacco, vapes, prescription medicine, or anything that breaks local law in a region you target. - Categories restricted by our partners — Some business types are restricted under the rules of our payment, banking and compliance partners. Where they restrict a category, we cannot support it. - Custodial finance & third-party funds — Unlicensed banking, escrow, lending, or customer stored-value and top-up balances you hold on behalf of your users. Ordinary supplier, vendor or driver payables that a platform owes and pays out of its own settled funds are not custodial and are not covered by this entry. - Company registration & statutory filing services — Processing or facilitating incorporations, government filings, licences, permits or other statutory registrations. - Tax evasion, shell entities or regulatory arbitrage — Products or setups designed to evade tax, mask ownership, or bypass financial and legal regulation. - Regulated professional services — Legal, accounting, medical and other licensed or certification-bound services — advisory work, diagnosis or treatment, document preparation, representation, tax planning. - Travel, immigration & visa services — Flight, cruise, charter and timeshare bookings, plus visa, residency or citizenship facilitation. - Health & wellness products — Diagnostics, weight-loss programmes, supplements or biohacking kits — including well-intentioned and honestly marketed ones. - Miracle, misleading or unverifiable claims — Anything promising outcomes that cannot be substantiated ("cures X", "reverses ageing", "make money while you sleep"). - Social matching & interaction services — Random video or chat matching, matchmaking, dating platforms, AI relationship companions. - Religious or spiritual services — Paid prayers, rituals, faith-based counselling, or paid access to a religious or spiritual authority. - Gaming & virtual goods — Online games, in-game currency, digital item sales, boosters or private servers — official or not. - Virtual asset services — Crypto offerings, custodial wallets, NFTs, DeFi, exchanges or token launches. - Piracy or IP violations — Selling anything you did not create or license — pirated media, software keys, copy-paste code packs. - IPTV & streaming access services — IPTV subscriptions, reseller panels, or tools granting access to third-party streaming content. - Software resale or licence flipping — Discounted licence resale, or reselling AI APIs, datasets or tools without clear rights. - Privacy violations & surveillance tech — Stalkerware, keyloggers, non-consensual location tracking, facial recognition, face-swap tooling, or scraping personal data without a legal basis. - Hosting, servers & infrastructure — VPN, VPS or dedicated servers, hosting plans, cloud credits, bandwidth resale, or any persistent server-level access sold to end users. - Telecommunication services — SIM and eSIM sales, VoIP, PSTN calling, telecom routing, call or SMS termination and related operations. - Spam, mass outreach & scraping tools — Lead scraping, bulk outreach or spam tooling, and databases of sensitive personal information. - Proxy, cloaking or anti-terms tools — Anything that helps users bypass rate limits, captcha, authentication, platform terms or geo restrictions. - Cheating & manipulation tools — Hacks, mods, bots, engagement manipulation, fake reviews, or tools that bypass platform or API restrictions. - Donations — Collecting money without delivering a defined product or service in return. - Fundraising — Charity or NGO drives, political or religious donations, personal fundraisers and crowdfunding. - Payment forwarding & money transmission — Acting purely as a conduit for other people’s money — collecting on behalf of unrelated businesses, unlicensed money transmission or remittance, and gift or benefit card resale. Platforms that are the merchant of record, settle to their own verified account and pay their own suppliers are supported; see "Platforms, marketplaces and on-demand services" below. - Ticket resale & speculative reservations — Resale of tickets, passes or reservations you did not issue, and speculative or scalped bookings. Selling or booking your own service, or bookings taken through your own platform, is supported. - Weapons & violence-oriented content — Weapons, combat training, DIY explosives, self-defence tools, or content that glorifies violence or incites harm. Even if your business is not listed above, we may still decline it where we believe it carries legal, financial or reputational risk. Being technically digital does not by itself make a product eligible. ## Businesses that require review Some categories are not prohibited, but need enhanced due diligence. We deliberately support high-risk-but-legal categories that mainstream processors blanket-ban — betting tips, gambling operators, Forex education and signals — provided the business is legal, honest with its customers and properly licensed where a licence is required. If you operate in one of these spaces we may ask for licences, disclaimers, demo access or policy links before approving your account. Approval is decided case by case and is not guaranteed. > **A supported category is not automatic approval** Every merchant is assessed individually. Supporting a category means we will consider businesses in it — each one still goes through KYC, KYB where applicable, a risk rating and sanctions/PEP screening, and higher-risk merchants go through enhanced due diligence. See our [AML, CTF & Sanctions Policy](https://webrabbitmedia.com/aml). - Platforms, marketplaces & on-demand services — Ride-hailing and e-hailing, courier and parcel delivery, food ordering, and booking platforms for your own service. Supported where you are the merchant of record and settle to your own verified business account — see the conditions below. - Betting tips, predictions & handicapping — Paid tips, picks, models or subscription communities for sports betting. We ask for clear odds/risk disclaimers, no guaranteed-win claims, an 18+ gate and visible refund terms. - Gambling & games of chance — Casinos, lotteries, betting operators, sweepstakes and cash-prize contests. We ask for your gaming licence (Gaming Commission of Ghana or the equivalent regulator in every market you serve) and responsible-gaming controls. - Forex, trading education & signals — Forex/CFD signals, trading courses, funded-account or prop-firm style products. We ask for risk disclosures, honest performance claims and any licence required in the markets you sell to. - Financial tools & advice — Budgeting, investing, wealth or money-management products. Licensed where the law requires it, with no guaranteed-return claims. - AI content generation tools (text, image, video, voice) — No impersonation, scraping or deepfakes. - Marketing & outreach tools — No spam, scraping or fake engagement. - Resume, hiring or exam tools — No impersonation or cheating functionality. - Spiritual & astrology services — Entertainment only — no claims or predictions. - Audio, music & chatbot generators — No voice cloning, NSFW output or IP infringement. - E-books & written digital publications — Guides, playbooks, reports and downloadable written content. - Productised services — A fixed, pre-defined digital deliverable that is identical for every buyer, with limited repeatable operational work — not custom or consultative. ### Conditions for platforms, marketplaces and on-demand services Ride-hailing, courier and delivery, food ordering and booking platforms are supported on our rails. The fact that the ride or the meal is delivered in the real world does not disqualify you — what matters is who the customer is paying and who is accountable for the service. The conditions are: - You are the merchant of record. The customer pays your platform for a service your platform is accountable for. We settle to your own verified business account only. - You pay your own suppliers. Paying drivers, riders, couriers, restaurants or vendors is your responsibility, made from your settled funds. We do not split, hold or route money to third parties on your behalf, and we do not treat those payables as custodial funds. - Licences where they are required. Any transport, e-hailing, courier, food handling or other permit needed to operate in each market you serve. - Clear customer terms. Published cancellation, refund and complaint terms, and a support channel that answers. - No customer stored value. Customer wallets, top-up balances or any product where you hold money for your users stays out of scope until you hold the licence that permits it. Talk to us before you build one. - Standard KYB, plus a look at the live product. Business registration, directors and bank verification, and a walk-through of the live app and pricing before you go live. ### What to prepare for an enhanced review For sensitive services — tipster and prediction platforms in particular — we run a short enhanced review before you go live. Approval is decided case by case and is usually quick. Having these ready speeds it up: - A link to your live platform and your pricing page. - The exact wording customers see about what they are buying. - Your refund policy. - An 18+ and responsible-gambling disclaimer on the sales page. - For Telegram or private-community delivery, we may ask for a quick look at the channel to confirm subscribers actually receive what they paid for. We may also come back with follow-up questions later — for example after a product change, a new domain, or a rise in disputes. We are an uncensored payment provider: we are not here to decide which legal business model deserves to exist. What we do insist on is transparency, so that every customer knows what they are buying and is satisfied with what they receive. What we will not support is money laundering, fraud, or deception dressed up as a product. Within that line, we will do our best to support your business model. ## Declaring where your API keys run Live integrations must declare the site and app using each API key via the `HTTP-Referer` and `X-Webrabbitmedia-Title` headers — see [Identify your app](https://webrabbitmedia.com/docs/authentication#identify-your-app). Declarations are reviewed by humans. False or misleading attribution can lead to suspension of the key or the account. If you move to a new domain or rebrand, notify us at support@webrabbitmedia.com before the change so your record stays accurate. ## Fulfilment & access delivery Where you use Web Rabbit Payments to deliver access on purchase — a licence key, a download bundle, a private community invite, a repository or a template — the standards in this policy apply to what is actually delivered, not only to what the checkout page advertises. Material drift between the advertised product and the gated content is treated as a policy breach and may lead to review, restriction or closure. ## Review & monitoring We combine automated checks with human review. Reviews happen at defined points in the lifecycle: - Activation & first transaction — confirming products and checkout match what was disclosed at onboarding. - Before first payout — completing compliance checks and validating fulfilment. - Ongoing triggers — new domains, product shifts, unusual transaction patterns, or rising disputes and refunds. - Periodic reviews — routine monitoring to confirm continued compliance. Possible outcomes are continued support, remediation (we ask you to fix or clarify something), or deboarding. ## Enforcement If we determine an account was onboarded in breach of this policy, we may take any of the following actions: - Suspend the account permanently - Halt transactions and platform access immediately - Withhold payouts or reverse settled funds - Refund transactions already processed - Report to financial partners or regulators where required ### How enforcement affects payouts Withholding or reversing settled funds is reserved for fraud, money laundering, serious chargeback or refund exposure, or a lawful order from a competent authority. It is not something we do during a routine review, a documentation request or a category clarification — those run while your payouts continue as normal. Where a genuine problem arises and the law allows it, we contact you first, explain what we have seen and give you a chance to respond before we hold funds. If you run a platform that owes drivers or vendors daily, tell us during onboarding and we will agree the payout rhythm up front. Common breaches include: - Misclassifying your business or products to bypass category restrictions. - Using proxy merchants or sub-accounts to avoid enforcement. - Selling deceptive or harmful products, or anything on the prohibited list. - Failing to disclose essential onboarding information, such as a website that does not match the declared business. - Unusual transaction volume, or a high rate of refunds, disputes or complaints. - Activity flagged by our payment, banking or compliance partners. - A lawful request or order from a competent authority. ## Appeals If you believe enforcement was applied in error, you can request a review by emailing [compliance@webrabbitmedia.com](mailto:compliance@webrabbitmedia.com). Reviews are considered once and the decision is final. Voluntary disclosure of a potential issue is considered favourably. > **Not sure where you fit?** Reach out before you build. See also the [onboarding & verification](https://webrabbitmedia.com/docs/onboarding) walkthrough, the [merchant eligibility](https://webrabbitmedia.com/docs/merchant-countries) and [accepted countries](https://webrabbitmedia.com/docs/accepted-countries) pages. --- # Onboarding & verification > Individual vs registered entity, the verification checklist, review statuses and going live. Every merchant account on Web Rabbit Payments starts the same way: create your workspace, pick an account type, complete verification, then switch from Test Mode to Live Mode. This page documents exactly what the dashboard asks for at each step, so you can gather everything before you start. ## Are you an individual or registered business? This is the first question in Merchant → Verification . It decides which checklist you get and which documents we ask for. You can change it from the verification page until you submit your business details. ### Individual - You're a creator or sole proprietor - Payments and payouts are tied to you personally - You don't have a company registration or business tax ID ### Registered entity - Your business is registered (Pvt Ltd, LLP, LLC, etc.) - You have a GST, EIN, or other business tax ID - You invoice customers under your company name > **Individuals are welcome** You do not need a registered company to accept payments with Web Rabbit. Individuals verify with a government-issued ID and a selfie, and settle to a personal bank account or mobile money wallet. Your country of eligibility comes from the ID you verify with — see [merchant eligibility](https://webrabbitmedia.com/docs/merchant-countries). ## Choosing your business category When you create a business you pick from 127 categories in these groups: Agriculture, Digital and software, Education, Financial services, Food and hospitality, Gaming and betting, Health and beauty, Media and entertainment, Non-profit and religious, Professional services, Real estate, Retail and e-commerce, Transport and logistics, Travel and events, Utilities and telecoms. Type to search, or choose “Something else” and describe it. Categories such as betting, Forex, lending and ride-hailing are supported but get a few extra checks during verification. Activities we can't support are listed in [merchant acceptance](https://webrabbitmedia.com/docs/merchant-acceptance). ## Your verification checklist The dashboard unlocks steps in order. Each step can be saved and resumed. | Account type | Steps | | --- | --- | | Individual | Product information → Identity → Payout destination | | Registered entity | Product information → Identity → Business details → Payout destination | ## Step 1 — Product information What you sell and how buyers receive it. The form asks for: - Website(s) where customers purchase or access your product (add as many as apply) - A short description of what the product does - Product category — SaaS, Digital goods, Online course, E-book, Template, Membership, Consulting or Other - How customers receive the product after payment (instant access, email delivery, manual fulfilment, ongoing subscription) plus a brief description of the flow - How automated delivery is — fully automated through to fully manual - Whether the product touches a sensitive area — crypto, health claims, regulated professional services, 18+ content, or gambling / betting tips / predictions and Forex, trading signals or trading education. The last two are supported and simply route you to an enhanced review. - How you intend to integrate — payment links, inline checkout, API/SDK, or undecided - How you acquire customers, and social media links for the product and founder - Product stage (Idea, Building, Beta, Live and selling) and your current payment platform, if any ## Step 2 — Identity Required for both account types. Takes about a minute. - Full legal name as it appears on your ID, and date of birth - Residential address — country, address lines, city, state/region, postal code - ID type (Passport, National ID or Driver's licence) and the document number - Photo of the ID — front and back where the document has two sides - A selfie for liveness matching Uploads accept PDF, JPG or PNG up to 10 MB and are stored in a private, access-controlled bucket. ## Step 3 — Business details (registered entities only) - Legal name, trading name, entity type, incorporation date, registration/company number, tax ID (EIN / GST / VAT) - Registered address, website, support email and support phone - Owner or primary director — full name, role, date of birth and ownership percentage - Documents: certificate of incorporation (or certificate of registration for a sole proprietorship), Form 3 for a company or Form A for a sole proprietorship, Ghana Card of the business owner, Ghana Cards for directors, and the tax document ## Step 4 — Payout destination Where settled funds land. Choose one of two destinations: | Destination | What we ask for | | --- | --- | | Mobile money | Network (MTN Mobile Money, Telecel Cash, AT Money, G-Money), wallet number and confirmation, registered name, currency and country. No document upload — the network name lookup proves ownership. | | Bank account | Account holder name, account number / IBAN and confirmation, routing type and code (IFSC, US routing or SWIFT/BIC), currency, bank name, branch name and address, country, plus a cancelled cheque or recent bank statement. | ## Review, statuses and go-live Once every step is submitted, your business moves into review. Most reviews finish within 72 hours. Statuses you will see in the dashboard: | Status | What it means | | --- | --- | | Pending | Submitted and queued for review. Test Mode stays fully usable. | | On hold | We need something corrected. The reason is shown inline on the affected step so you can fix and resubmit that step only. | | Approved | Live Mode unlocks. Live keys, live collections and payouts become available. | > **Test Mode is fully simulated** Test Mode is isolated from live data and never settles funds to your balance. Test charges are answered by Web Rabbit's built-in simulator — nothing reaches a payment provider, no prompt is sent to a real handset, no wallet is debited, and amounts are capped at GHS 10. See [test data](https://webrabbitmedia.com/docs/test-data). Sensitive categories — tipster and prediction platforms, gambling operators, Forex signals and education — go through a short enhanced review. The checklist of what to prepare is on the [Merchant Acceptance Policy](https://webrabbitmedia.com/docs/merchant-acceptance#enhanced-review-checklist) page. ## After approval - API keys — create live and test keys under Developer → API keys. Keys are shown once; authenticate as described in Authentication. - Declare your app — send HTTP-Referer and X-Webrabbitmedia-Title with live requests so we know where each key runs. Declarations appear under Developer → Applications and are reviewed by humans. - Start collecting — use Mobile Money, Hosted Checkout, or payment links generated from the dashboard. - Automate fulfilment — subscribe to webhooks; every delivery is signed with HMAC-SHA256 so your backend can verify a payment before granting access. - Get paid out — a platform fee from 10% applies to successful collections (see Fees). Withdrawals settle in GHS from Payouts → Balances, with a minimum withdrawal of GHS 2,000. A flat payout fee of 1.5% (no minimum or maximum) is deducted from the amount you request. - Payout limits — every business has limits that grow with trust. New: GHS 5,000 per payout, GHS 10,000 per day, GHS 50,000 per month. Standard: GHS 20,000 / 40,000 / 300,000. Trusted: GHS 50,000 / 100,000 / 1,000,000. Bulk disbursements count toward the daily and monthly amounts. Large payouts get an extra review, withdrawals pause for 24 hours after a payout account is added or changed, and at most 3 payouts can wait at once. Your current limits show on Payouts → Balances; contact support on +233 59 896 9229 to raise them. - Payout days — payouts and bulk disbursements are processed on working days only (Monday to Friday, excluding Ghana public holidays). Requests made on a weekend or holiday are scheduled for the next working day. - Outside Ghana — approved merchants in all 16 registration countries can go live. Payments and payouts there run on OGateway in the local currency; the same limits, reviews and working-day rules apply. > **Keep your record accurate** If you move to a new domain, rebrand, or materially change what you sell, tell us at support@webrabbitmedia.com before the change. False or stale declarations can lead to suspension of the key or the account. --- # Rabbit AI assistant > Ask, Build and Plan modes, dashboard actions, and auto-approval settings. Rabbit AI is the assistant built into your merchant dashboard at Merchant → Rabbit AI . It answers from these docs and from your own account data, and it can carry out a small set of dashboard actions once you confirm them. It only ever sees the business you are in and the environment you are in — Test and Live data are never mixed. ## Three ways of working Pick a mode above the message box. Each conversation remembers the mode it was started in. | Mode | Use it for | What you get back | | --- | --- | --- | | Ask | Questions about your account, payments, messaging, fees and policy. | A direct answer, with the figures read live from your account and links to the docs page behind it. | | Build | Integrating Web Rabbit into your own application. | A setup checklist, complete runnable code with the real endpoints and headers, and how to test it before going Live. | | Plan | Designing a flow before you write any of it. | The recommended architecture step by step, the trade-offs of the alternative, a sequenced checklist, and what to watch in production. No code, and no actions. | ## Example tasks - Integrate a checkout page — a hosted checkout or payment link, wired to your product list. - Implement a webhook — a handler for payment events, including signature verification. - Collect mobile money from your app — the charge call, the pending state, and the callback. - Set up OTP by SMS — send a code, verify it, and handle expiry and resends. - Design a refunds and reconciliation process — who does what, and in what order. ## Actions and confirmation In Ask and Build modes, Rabbit AI can propose an action: create a payment link, charge a Mobile Money customer, send an SMS, retry a webhook delivery, add a product, create a discount code, refund a payment, withdraw a balance or buy airtime. It never runs one silently — you see a card describing exactly what will happen, including the currency, and you press Confirm. API keys, bulk disbursements, invoices and verification documents stay on their dashboard pages. Withdrawals choose one currency balance and go to your payout account in that currency. Refunds always use the payment's own currency, and the card shows the refund fee taken from that balance. Payment links, charges from chat, products, discounts and airtime are in GHS today. Only the business owner can run actions. Team members can ask anything, but cannot confirm. ## Auto-approval The owner can let Rabbit AI run some actions without asking, from the gear icon next to the history button. The setting is saved per business and applies to everyone in that workspace. | Setting | Test mode | Live mode | | --- | --- | --- | | None (default) | Every action asks you first. | Every action asks you first. | | Low risk | Sending an SMS, retrying a webhook, adding a product or creating a discount runs automatically. Anything that moves money still asks. | Every action asks you first. | | All | Everything runs automatically. Test mode never moves real money. | Only actions that cannot move money run automatically. Payment links, charges, refunds, withdrawals and airtime still ask. | Anything that can move real money in Live mode always waits for your confirmation, whatever the setting says. Every automatic run is written to your workspace activity log with the action, the environment and the setting that allowed it. ## Several currencies If your business collects in more than one country, Rabbit AI answers per currency: each balance, sales total and customer total is shown in its own currency and never added together. When you don't name a currency it uses the one selected on your dashboard. Ask "which countries can I collect in?" to see methods, networks and payout options per country. ## What Rabbit AI can see Rabbit AI reads your payments, refunds, payouts, balances per currency, currency conversions, payment countries, brands, sales, customers, products, orders, invoices, discounts, storefronts, bill purchases, disbursements, webhook deliveries, messaging usage, SMS Marketing campaigns and package credits, payout schedule and verification status — for the current business and the current environment only. It cannot read your API key secrets, and conversations are stored against your own account so you can return to them from the history list. --- # Payment methods by country > Mobile Money networks, Nigeria bank transfer and failure codes for each country. Hosted checkout shows the right payment options for the merchant's country automatically. Outside Ghana these run on our OGateway rail. > **Several countries from one business** Every business collects in its registered country's currency. Web Rabbit can also enable extra countries for your business — for example a Ghana business collecting NGN in Nigeria and KES in Kenya. Pass `country` (or `currency`) on `/v1/checkout/session` or `/v1/collect/momo`. Each currency keeps its own balance and is paid out in that currency; nothing is converted. A country that isn't enabled returns `422 currency_not_enabled`. See `GET /v1/me` → `enabled.countries`. Card stays GHS-only; gaming merchants can't be enabled in Benin, Côte d'Ivoire, Burkina Faso, Senegal, Zambia, Cameroon, Mali or Togo. > **Selling in each currency from the dashboard** Payment links, products, storefronts, invoices and discounts each have a currency, picked from the countries enabled for your business. A storefront sells in one currency and only lists products priced in it. A percentage discount can work in any currency; a fixed-amount discount or minimum order applies only to orders in its own currency. Orders, invoices and payment links show and total each currency separately. Charging a phone directly from Sales → Collect is Ghana Mobile Money; for other currencies, send a payment link in that currency. Live payments and payouts are open in all 16 registration countries once a business is approved. Payouts outside Ghana are sent in the local currency through OGateway. Congo-Brazzaville has no Mobile Money option yet, so its checkout shows no payment method for now. ## Countries and methods | Country | Currency | Customers pay with | Payouts | Status | | --- | --- | --- | --- | --- | | Ghana | GHS | MTN, Telecel, AirtelTigo | Mobile Money and bank | Live | | Nigeria | NGN | Bank transfer (temporary account) | Bank | Live | | Kenya | KES | M-Pesa, Airtel, Equitel, Telkom | Mobile Money and bank | Live | | Uganda | UGX | MTN, Airtel | Mobile Money | Live | | Tanzania | TZS | Vodacom, Airtel, Tigo | Mobile Money | Live | | Zambia | ZMW | MTN, Airtel, Zamtel | Mobile Money | Live | | Côte d'Ivoire | XOF | Orange, MTN, Moov, Wave | Mobile Money | Live | | Cameroon | XAF | MTN, Orange | Mobile Money | Live | | Gambia | GMD | QMoney, Afrimoney, Wave | Mobile Money | Live | | Congo DRC | CDF | M-Pesa, Airtel, Orange | Mobile Money | Live | | Benin | XOF | MTN, Moov | Mobile Money | Live | | Burkina Faso | XOF | Orange, Moov, Mobicash | Mobile Money | Live | | Mali | XOF | Orange, Moov, Mobicash | Mobile Money | Live | | Senegal | XOF | Orange, Wave, Free, E-Money | Mobile Money | Live | | Togo | XOF | T-Money, Moov | Mobile Money | Live | | Congo-Brazzaville | XAF | Not yet available | Not yet available | Waiting on provider | ## Nigeria bank transfer The customer chooses Bank transfer and gets a temporary account number for that payment only. It expires after about 25 minutes. The payment is confirmed only once the money arrives and we have re-checked it with the provider. Your `transaction.succeeded` webhook fires the same way as for Mobile Money. ## Why a payment failed Failed payments carry a plain-language `reason`, plus the provider code and who caused the failure in your dashboard's payment details. | Code | Caused by | Meaning | | --- | --- | --- | | 4100 | Customer | Not enough balance, or a wallet limit was reached. | | 4200 | Customer | Prompt not answered in time, or wrong PIN. | | 5100 | Merchant | Number not registered on that network, or wallet inactive. | | 5200 | Merchant | Invalid account number. | | 5210 | Merchant | Amount not allowed for this wallet. | | 3200 / 6500 | Switch / Provider | Network or provider busy. Retry in a few minutes. | | 6100 | Provider | Duplicate. Wait before retrying. | | 6400 | Provider | Provider float low. Retry later. | | 7100 | OGateway | Processing failed at OGateway. | --- # Merchant eligibility > Where you can register a business and receive payouts. Live in all 16 registration countries. This page covers where you can register a merchant account and receive payouts. It is different from the [countries your customers can pay from](https://webrabbitmedia.com/docs/accepted-countries). ## Where we onboard merchants today > **Live payments and payouts in all 16 countries** Approved merchants in every country below can register, verify, take live payments and receive payouts in their local currency. Ghana settles in GHS; other countries run on our OGateway rail. - 🇬🇭 Ghana — gaming/betting: allowed after KYC is shared with our partner - 🇰🇪 Kenya — gaming/betting: not available (low-risk only) - 🇳🇬 Nigeria — gaming/betting: allowed - 🇹🇿 Tanzania — gaming/betting: allowed - 🇺🇬 Uganda — gaming/betting: allowed - 🇿🇲 Zambia — gaming/betting: not available (low-risk only) - 🇧🇯 Benin — gaming/betting: not available (low-risk only) - 🇨🇮 Côte d'Ivoire — gaming/betting: not available (low-risk only) - 🇧🇫 Burkina Faso — gaming/betting: not available (low-risk only) - 🇨🇲 Cameroon — gaming/betting: not available (low-risk only) - 🇲🇱 ML — gaming/betting: not available (low-risk only) - 🇸🇳 Senegal — gaming/betting: not available (low-risk only) - 🇹🇬 Togo — gaming/betting: not available (low-risk only) - 🇬🇲 Gambia — gaming/betting: allowed - 🇨🇩 Congo DRC — gaming/betting: allowed - 🇨🇬 Congo-Brazzaville — gaming/betting: allowed If your country is not listed, you cannot register a merchant account yet. ## How eligibility is determined Eligibility is based on the country that issued the government-issued identity document you verify with — not where your company is registered, and not where you pay tax. | Account type | Whose ID decides eligibility | | --- | --- | | Individual | Your own government-issued ID. | | Registered entity | The government-issued ID of every director and beneficial owner, in addition to the country of incorporation. | > **Incorporation alone is not enough** Registering a company in a supported country does not by itself make an account eligible. If a director or beneficial owner can only verify with an ID issued elsewhere, the account cannot be onboarded. Tax residence, residence permits and local bank accounts are helpful, but none of them replace a government-issued ID from a supported country. ## Planned expansion These markets are on our roadmap, subject to partner coverage and local licensing. Nothing here is a commitment to a launch date. - South Africa - Rwanda - Sierra Leone - Liberia - United Kingdom - United States - Canada - United Arab Emirates ## Restricted countries Any country not listed under "Where we onboard merchants today" is restricted for merchant registration and payouts right now. In addition, we permanently cannot onboard merchants in countries or territories subject to active sanctions or to restrictions imposed by FATF, OFAC, UN or EU AML/CFT directives — those remain excluded even as we expand. Payments can still be accepted from customers in 227 countries and territories. See [countries eligible for payment acceptance](https://webrabbitmedia.com/docs/accepted-countries). > **Questions before you integrate** Email [support@webrabbitmedia.com](mailto:support@webrabbitmedia.com) so you don't build against an outcome that cannot be approved. Read the [Merchant Acceptance Policy](https://webrabbitmedia.com/docs/merchant-acceptance) alongside this page. --- # Accepted countries > The countries and territories your customers can pay from. The list below includes the countries and territories your customers can pay you from. Card and Mobile Money payments originating outside this list are declined at authorisation. ## How it works Payer eligibility is determined by the country of the payment instrument (the card issuer country, or the mobile money operator's country), not by the customer's billing address or IP. Settlement to your Web Rabbit balance is always in GHS , regardless of where the payment originated. > **Merchant eligibility is separate** This page is about where your customers can pay from. Where you can register a business and receive payouts is covered on the merchant eligibility page. ## Accepted countries & territories 227 countries and territories are currently supported for payment acceptance. | # | ISO | Country / territory | | --- | --- | --- | | 1 | AX | Åland Islands | | 2 | AL | Albania | | 3 | DZ | Algeria | | 4 | AS | American Samoa | | 5 | AD | Andorra | | 6 | AO | Angola | | 7 | AI | Anguilla | | 8 | AG | Antigua and Barbuda | | 9 | AR | Argentina | | 10 | AM | Armenia | | 11 | AW | Aruba | | 12 | AU | Australia | | 13 | AT | Austria | | 14 | AZ | Azerbaijan | | 15 | BS | Bahamas | | 16 | BH | Bahrain | | 17 | BD | Bangladesh | | 18 | BB | Barbados | | 19 | BE | Belgium | | 20 | BZ | Belize | | 21 | BJ | Benin | | 22 | BM | Bermuda | | 23 | BT | Bhutan | | 24 | BO | Bolivia | | 25 | BA | Bosnia and Herzegovina | | 26 | BW | Botswana | | 27 | BV | Bouvet Island | | 28 | BR | Brazil | | 29 | IO | British Indian Ocean Territory | | 30 | VG | British Virgin Islands | | 31 | BN | Brunei Darussalam | | 32 | BG | Bulgaria | | 33 | BF | Burkina Faso | | 34 | BI | Burundi | | 35 | KH | Cambodia | | 36 | CM | Cameroon | | 37 | CA | Canada | | 38 | CV | Cape Verde | | 39 | BQ | Caribbean Netherlands | | 40 | KY | Cayman Islands | | 41 | TD | Chad | | 42 | CL | Chile | | 43 | CN | China | | 44 | CX | Christmas Island | | 45 | CC | Cocos (Keeling) Islands | | 46 | CO | Colombia | | 47 | KM | Comoros | | 48 | CD | Congo DRC | | 49 | CG | Congo-Brazzaville | | 50 | CK | Cook Islands | | 51 | CR | Costa Rica | | 52 | CI | Côte d'Ivoire | | 53 | HR | Croatia | | 54 | CW | Curaçao | | 55 | CY | Cyprus | | 56 | CZ | Czech Republic | | 57 | DK | Denmark | | 58 | DJ | Djibouti | | 59 | DM | Dominica | | 60 | DO | Dominican Republic | | 61 | EC | Ecuador | | 62 | EG | Egypt | | 63 | SV | El Salvador | | 64 | GQ | Equatorial Guinea | | 65 | ER | Eritrea | | 66 | EE | Estonia | | 67 | SZ | Eswatini | | 68 | ET | Ethiopia | | 69 | FK | Falkland Islands | | 70 | FO | Faroe Islands | | 71 | FJ | Fiji | | 72 | FI | Finland | | 73 | FR | France | | 74 | GF | French Guiana | | 75 | PF | French Polynesia | | 76 | TF | French Southern Territories | | 77 | GA | Gabon | | 78 | GM | Gambia | | 79 | GE | Georgia | | 80 | DE | Germany | | 81 | GH | Ghana | | 82 | GI | Gibraltar | | 83 | GR | Greece | | 84 | GL | Greenland | | 85 | GD | Grenada | | 86 | GP | Guadeloupe | | 87 | GU | Guam | | 88 | GT | Guatemala | | 89 | GG | Guernsey | | 90 | GN | Guinea | | 91 | GW | Guinea-Bissau | | 92 | GY | Guyana | | 93 | HM | Heard Island and McDonald Islands | | 94 | HN | Honduras | | 95 | HK | Hong Kong | | 96 | HU | Hungary | | 97 | IS | Iceland | | 98 | IN | India | | 99 | ID | Indonesia | | 100 | IQ | Iraq | | 101 | IE | Ireland | | 102 | IM | Isle of Man | | 103 | IL | Israel | | 104 | IT | Italy | | 105 | JM | Jamaica | | 106 | JP | Japan | | 107 | JE | Jersey | | 108 | JO | Jordan | | 109 | KZ | Kazakhstan | | 110 | KE | Kenya | | 111 | KI | Kiribati | | 112 | KW | Kuwait | | 113 | KG | Kyrgyzstan | | 114 | LA | Laos | | 115 | LV | Latvia | | 116 | LB | Lebanon | | 117 | LS | Lesotho | | 118 | LR | Liberia | | 119 | LI | Liechtenstein | | 120 | LT | Lithuania | | 121 | LU | Luxembourg | | 122 | MO | Macau | | 123 | MG | Madagascar | | 124 | MW | Malawi | | 125 | MY | Malaysia | | 126 | MV | Maldives | | 127 | MT | Malta | | 128 | MH | Marshall Islands | | 129 | MQ | Martinique | | 130 | MR | Mauritania | | 131 | MU | Mauritius | | 132 | YT | Mayotte | | 133 | MX | Mexico | | 134 | FM | Micronesia | | 135 | MC | Monaco | | 136 | MN | Mongolia | | 137 | ME | Montenegro | | 138 | MS | Montserrat | | 139 | MA | Morocco | | 140 | MZ | Mozambique | | 141 | NA | Namibia | | 142 | NR | Nauru | | 143 | NP | Nepal | | 144 | NL | Netherlands | | 145 | NC | New Caledonia | | 146 | NZ | New Zealand | | 147 | NI | Nicaragua | | 148 | NE | Niger | | 149 | NG | Nigeria | | 150 | NU | Niue | | 151 | NF | Norfolk Island | | 152 | MK | North Macedonia | | 153 | MP | Northern Mariana Islands | | 154 | NO | Norway | | 155 | OM | Oman | | 156 | PK | Pakistan | | 157 | PW | Palau | | 158 | PA | Panama | | 159 | PG | Papua New Guinea | | 160 | PY | Paraguay | | 161 | PE | Peru | | 162 | PH | Philippines | | 163 | PN | Pitcairn Islands | | 164 | PL | Poland | | 165 | PT | Portugal | | 166 | PR | Puerto Rico | | 167 | QA | Qatar | | 168 | RE | Réunion | | 169 | RO | Romania | | 170 | RW | Rwanda | | 171 | BL | Saint Barthélemy | | 172 | SH | Saint Helena | | 173 | KN | Saint Kitts and Nevis | | 174 | LC | Saint Lucia | | 175 | MF | Saint Martin | | 176 | PM | Saint Pierre and Miquelon | | 177 | VC | Saint Vincent and the Grenadines | | 178 | WS | Samoa | | 179 | SM | San Marino | | 180 | ST | São Tomé and Príncipe | | 181 | SA | Saudi Arabia | | 182 | SN | Senegal | | 183 | RS | Serbia | | 184 | SC | Seychelles | | 185 | SL | Sierra Leone | | 186 | SG | Singapore | | 187 | SX | Sint Maarten | | 188 | SK | Slovakia | | 189 | SI | Slovenia | | 190 | SB | Solomon Islands | | 191 | ZA | South Africa | | 192 | GS | South Georgia and the South Sandwich Islands | | 193 | KR | South Korea | | 194 | ES | Spain | | 195 | LK | Sri Lanka | | 196 | SR | Suriname | | 197 | SJ | Svalbard and Jan Mayen | | 198 | SE | Sweden | | 199 | CH | Switzerland | | 200 | TW | Taiwan | | 201 | TJ | Tajikistan | | 202 | TZ | Tanzania | | 203 | TH | Thailand | | 204 | TL | Timor-Leste | | 205 | TG | Togo | | 206 | TK | Tokelau | | 207 | TO | Tonga | | 208 | TT | Trinidad and Tobago | | 209 | TN | Tunisia | | 210 | TR | Turkey | | 211 | TM | Turkmenistan | | 212 | TC | Turks and Caicos Islands | | 213 | TV | Tuvalu | | 214 | VI | U.S. Virgin Islands | | 215 | UG | Uganda | | 216 | AE | United Arab Emirates | | 217 | GB | United Kingdom | | 218 | US | United States | | 219 | UM | United States Minor Outlying Islands | | 220 | UY | Uruguay | | 221 | UZ | Uzbekistan | | 222 | VU | Vanuatu | | 223 | VA | Vatican City | | 224 | VN | Vietnam | | 225 | WF | Wallis and Futuna | | 226 | EH | Western Sahara | | 227 | ZM | Zambia | › This list changes We update the list as we expand coverage and as our payment and compliance partners adjust their own supported regions. Build against the API response, not a hardcoded copy of this table. ---