# Merchant payment API Base URL: `https://payments.rhedux.design`. Flutterwave is the primary LIVE provider until direct MTN/Airtel routes are approved. Uganda uses UGX mobile money, Kenya KES M-Pesa, Rwanda RWF mobile money, and other countries USD cards. TEST keys keep their configured direct mobile-money sandbox route. ## Flutterwave LIVE checkout Use a LIVE `x-api-key` and a stable `Idempotency-Key` from the merchant backend. Send whole-unit `amount`, `currency`, optional ISO `country`, `method` (`MOBILE_MONEY` for UGX/KES/RWF or `CARD` for USD), `customer.email`, optional `customer.name` and `phoneNumber`, `description`, `merchantReference`, `successUrl` and `cancelUrl`. Email is required. Prices and order IDs must come from your server. Dotpay does not invent exchange rates. The existing integer ledger accepts positive whole currency units. Register your application's exact HTTPS return origins before sending return URLs. Arbitrary domains, credential-bearing URLs and HTTP are rejected. Open the response's `checkoutUrl` exactly as returned. Uganda checkout uses `https://payments.rhedux.design/checkout/?token=...`, where the payer selects MTN MoMo or Airtel Money and confirms their phone number. Other currencies retain Flutterwave-hosted checkout. The callback address to Flutterwave is generated and signed by Dotpay, and verifies the stored payment before redirecting to the stored vendor return URL. Callback query parameters never prove payment. Keep checking your order after returning if settlement is still pending. Creating Uganda checkout does not initiate a wallet debit. The payer's Pay action submits the documented v3 `/charges?type=mobile_money_uganda` body: `phone_number`, explicit `network` (`MTN` or `AIRTEL`), immutable `amount`, `currency: UGX`, server-stored `email`, `tx_ref`, optional `fullname`, and Dotpay's signed `redirect_url`. Secrets stay server-side. Provider authorization redirects are preserved, and the payer completes any provider confirmation. Dotpay polls the original reference and returns the payer to the stored merchant URL after verified success. A timeout or malformed acceptance response stays pending; reopening checkout does not issue another debit. A definite initiation rejection is logged with its HTTP status and provider reason and permits a corrected submission after a short cooldown. Never fulfil from an initiation response. LIVE options include `provider: "FLUTTERWAVE"`, `hosted: true`, `ready` and country/currency/method pairs. Compatibility networks describe Uganda mobile money through Flutterwave, not approval of the direct MTN/Airtel adapters. Provider account methods must be enabled. An existing Flutterwave webhook on another domain can remain there: Dotpay polls `/v3/transactions/verify_by_reference?tx_ref=...` independently. The optional `/api/webhooks/flutterwave` validates v3 `verif-hash` before verifying through the provider API. The hosted checkout response contains `referenceId`, `mode`, `amount`, `currency`, `merchantReference`, `status`, `checkoutUrl` and `retryAfterSeconds`. It acknowledges an attempt rather than receipt of money. Poll the normal authenticated status route at the requested interval. Only `paid=true` with matching reference, LIVE mode, amount, currency and merchant reference permits fulfilment. Repeated checks settle once and preserve refunds. Operational details and official references are in `docs/Flutterwave.md`. ## Authentication and environments In the portal, open **Keys**. Select your Sandbox account, use **Get key** for an existing active key or **Create key** for a new one, then **Copy key**. Open **Try a payment**, paste your sandbox key, select MTN and a result, and submit. Use **Check result** to see the outcome; open **Records** in Sandbox mode for the payment and activity history. Pending merchants can do this before live activation. Live keys are available only for approved live applications. Key listing/retrieval is scoped to your business and never exposes another merchant's credentials. Generate a Dotpay key in **Documentation → Integration access**. Send `x-api-key` from your backend. `dpk_test_…` belongs to a TEST application; `dpk_live_…` belongs to a separate LIVE application. The database key record and application decide the environment, not a request body. Never put keys in browser code, URLs, analytics or logs. Provider subscription keys, API users and Airtel credentials belong only to Dotpay's server configuration. Pending merchants may use TEST; only ACTIVE merchants may use LIVE. Restricted, suspended, disabled and revoked access is rejected. Activation preserves sandbox applications and keys. MTN TEST uses EUR and official fixture phone numbers; approved MTN LIVE Uganda uses UGX. Airtel Uganda uses UGX but requires provider approval, credentials, test wallets and allowed server IPs. ## Available payment options `GET /api/v_01/vendor/payments/options` with your server's `x-api-key` returns the authenticated application's mode and each network's currency and configuration status: ```json {"ok":true,"mode":"LIVE","networks":[{"network":"MTN","currency":"UGX","configured":false},{"network":"AIRTEL","currency":"UGX","configured":false}]} ``` Check this before creating a local purchase order. Offer only configured networks matching the purchase currency and LIVE mode for real purchases. This checks server configuration, not provider uptime, wallet eligibility or approval; collection can still fail. Responses contain no credentials and are not cached. Missing or blocked access uses the same authentication errors as checkout. ## Start payment `POST /api/v_01/vendor/payments/checkout` Headers: `Content-Type: application/json`, `x-api-key: `, `Idempotency-Key: `. ```json { "amount": 25000, "currency": "UGX", "method": "MOBILE_MONEY", "network": "MTN", "phoneNumber": "256772123456", "description": "Order payment", "merchantReference": "order-123" } ``` For the direct-provider TEST or approved direct LIVE route, `network` is MTN or AIRTEL. `amount` is a positive whole amount, up to 2147483647. Uganda phone numbers accept local 07… or international 2567… form. Use server-stored prices and order IDs. Description is truncated to 80 characters and merchant reference to 120. The example below illustrates direct collection; primary LIVE hosted checkout also requires `customer.email` and returns `checkoutUrl`. Accepted response (HTTP 202): ```json { "ok": true, "provider": "DOTPAY", "referenceId": "provider-attempt-uuid", "mode": "LIVE", "amount": 25000, "currency": "UGX", "merchantReference": "order-123", "status": "PROCESSING", "retryAfterSeconds": 5, "message": "Approve the payment on your phone." } ``` 202 acknowledges an attempt, not receipt of money. An upstream timeout can occur after acceptance. Save `referenceId`; poll status rather than create a new collection. Retry the same payload with the same idempotency key after a lost response. Reusing it with changed amount, currency, phone, network, link or merchant reference returns 409. Concurrent retries can initially return 409; wait and repeat the same request. A new identifier can debit again. ## Verify payment `GET /api/v_01/vendor/payments/status/{referenceId}` with the same application's `x-api-key`. ```json { "ok": true, "provider": "DOTPAY", "referenceId": "provider-attempt-uuid", "mode": "LIVE", "status": "SUCCEEDED", "paid": true, "failed": false, "amount": 25000, "currency": "UGX", "merchantReference": "order-123", "transactionId": "provider-transaction-id", "retryAfterSeconds": 0 } ``` Fulfil only when paid is exactly true and reference, LIVE mode, amount, currency and merchantReference match your stored order. Finalize your order and entitlement atomically; repeated verification must not grant duplicate access. PROCESSING/PENDING grants nothing. FAILED grants nothing. Refunded/cancelled records do not become successful because of a late status response. Existing archived payments stay in Records and may return 409 from this direct-provider status route. Respect retryAfterSeconds. Airtel's first enquiry waits at least 180 seconds, enforced server-side. MTN pending checks use at least five seconds. There is currently no merchant webhook delivery contract: poll status and run `npm run payments:reconcile` from a scheduled worker with an environment configured for this database. Browser redirects and provider callbacks are not settlement authority. The Uganda Flutterwave checkout keeps the Dotpay page open while provider authorization uses a separate window. Dotpay continues verification through slow phone approvals, checks again when the payer returns, closes its own authorization window after verified success, and follows the stored signed merchant return. If popups are blocked, the payer can use Continue payment. Reopening an already settled Dotpay checkout also returns to the merchant automatically. Popup closure and browser focus never prove receipt of money. ## Shared sandbox `POST /api/sandbox/payments` takes the same collection fields and headers, requires a TEST key and rejects LIVE keys. `GET /api/sandbox/payments?reference={referenceId}` verifies the attempt. Your own merchant's TEST key also works with the standard checkout/status endpoints. MTN fixtures with currency EUR: paid `56733123453`; failed `46733123450`; rejected `46733123451`; timeout `46733123452`; waiting `46733123454`. Airtel test wallet numbers must come from Airtel onboarding; these MTN fixtures are not Airtel numbers. A TEST result is simulated and proves no receipt of funds. Keep it separate from live financial records. Storefronts normally use it only for test access; the explicitly enabled TomDeriver trial below is an exception authorized by its owner. Sandbox payment links and requests use the normal hosted checkout. Choose Paid or Failed, enter the recipient's number, and try the payment. `POST /api/pay/{token}` accepts `testOutcome: "paid" | "failed"` only on MTN TEST links. Recipient validation still applies; the server replaces the number with the official fixture for the provider request. LIVE links reject this parameter. No user's phone is charged. ## Errors All errors return `{ "ok": false, "error": "short message" }`. | HTTP | Meaning | Action | | --- | --- | --- | | 400 | Invalid fields, network, currency or identifier | Fix before resubmitting | | 401 | Missing, invalid, expired, revoked or mismatched-mode key | Correct server configuration | | 403 | Account/app blocked or LIVE key sent to sandbox | Review access | | 404 | Payment absent or belongs to another application | Check stored reference and key | | 409 | Idempotency conflict, concurrent attempt, reserved link or archived payment | Reconcile existing request | | 422 | Provider explicitly refused initiation | Do not grant access | | 503 | Required provider configuration unavailable before initiation | Complete provider setup | | 500/502 or transport timeout | Indeterminate result | Keep order unresolved; use the same identifier and reconcile | ## TomDeriver integration Production base is the same URL above. `DOTPAY_VENDOR_API_KEY` remains the existing LIVE key. `DOTPAY_SANDBOX_API_KEY` is a separate TEST key for the same merchant. With `DOTPAY_CHECKOUT_MODE=TEST` and `DOTPAY_TEST_GRANT_ACCESS=true`, normal signed-in users can choose a plan or catalogue and complete MTN sandbox checkout. This is an owner-authorized trial on the deployed site: a verified successful test grants actual catalogue/service access without charging money. Failed or mismatched results grant nothing. Each order pins TEST mode, explicit access authorization, provider amount EUR 1 and its merchant reference in the server-created snapshot. Verification uses that pinned environment even after settings change. Existing LIVE orders still refuse TEST results. Payment/subscription money-received amounts for these grants are zero, with TEST metadata and the quoted UGX price retained separately. Normal production purchases now use `DOTPAY_CHECKOUT_MODE=LIVE` and `DOTPAY_TEST_GRANT_ACCESS=false`, with real UGX mobile-money or USD card prices. TomDeriver creates an account-owned order before checkout, passes an order-bound return token whose hash is stored on that order, and verifies its original owner's payment even if the external browser has no account cookie. Web/native clients refresh pending orders automatically. Previous authorized TEST purchases retain their pinned mode and granted access; new LIVE orders cannot accept TEST results. Admin-only diagnostics remain available and grant no access. Fulfilment binds mode, reference, amount, currency and merchant reference and runs transactionally once. Provider approval is separate from website deployment. Deploying to the production domain does not make sandbox credentials live credentials.