# RohoPay API — LLM Reference # Docs: https://docs.rohopay.com | Standard: https://llmstxt.org # Keep in sync with: apps/api/internal/routes/routes.go and DOCS-UPDATE.md ## Overview RohoPay is a payment API for East Africa. Supports mobile money (MTN MoMo, Airtel, M-Pesa, Tigo) and Visa/Mastercard via Relworx. Sells digital products via shareable payment links. Merchant wallet with top-up, withdrawal, and P2P transfers. ## Platform URLs - API: https://api.rohopay.com (port 8080) - Dashboard: https://payments.rohopay.com (port 3001) - Digital store: https://products.rohopay.com (port 3004) - WordPress: https://plugins.rohopay.com (port 3002) - Website: https://rohopay.com (port 3000) ## Authentication - `/api/v1/*` endpoints: Authorization: Bearer {api_key} - `/api/v1/checkout`: api_key goes in the REQUEST BODY (not Authorization header) - `/dashboard/*` and `/api/v2/*`: session cookie (dashboard login) - `/api/v2/public/*`: no auth required - Key prefixes: test_ = sandbox, live_ = production ## Idempotency (REQUIRED for mutations) - POST /api/v1/collect: MUST include Idempotency-Key: {uuid4} - POST /api/v1/disburse: MUST include Idempotency-Key: {uuid4} - Missing key → 400 MISSING_IDEMPOTENCY_KEY ## Core Endpoints POST /api/v1/collect Auth: Bearer {api_key} + Idempotency-Key header Body: { phone, amount (int), currency, description?, callback_url? } Returns: { status: "pending", internal_reference: "RHP-...", ... } Note: status always starts pending; resolves via webhook or polling POST /api/v1/disburse Auth: Bearer {live_api_key} — BLOCKED with test_ keys Idempotency-Key required Body: same as collect Note: requires sufficient live wallet balance GET /api/v1/transactions/{reference} Auth: Bearer {api_key} reference: internal_reference (RHP-...) or provider_reference Terminal statuses: "successful", "failed" (never change once set) GET /api/v1/transactions Auth: Bearer {api_key} Query: status, type, environment, from, to, limit, offset GET /api/v1/wallet/balance Auth: Bearer {api_key} Returns: { balance_live, balance_test, currency } POST /api/v1/checkout (Card payment — Visa/Mastercard) NO Authorization header — api_key goes IN THE REQUEST BODY Body: { api_key, amount, currency, customer_name, customer_email, return_url, card_number, card_expiry (MM/YY), card_cvv } Returns: { payment_url, internal_reference, status: "pending" } After: redirect user to payment_url (window.location.href, NOT iframe) On return: poll /api/v1/transactions/:ref — do NOT trust ?status= query param ## Phone Number Format Pattern: ^(256|254|255|250)[0-9]{7,12}$ - Uganda (MTN/Airtel): 256700123456 - Kenya (M-Pesa/Airtel): 254712345678 - Tanzania (M-Pesa/Tigo): 255712345678 - Rwanda (MTN/Airtel): 250780123456 Normalize: remove leading 0, remove +, add country code ## Transaction Status Flow pending → successful (terminal ✅) pending → failed (terminal ❌) Never: successful/failed → pending ## Webhook Events (outgoing from RohoPay to your callback_url) Header: x-rohopay-signature: sha256={hmac_hex} Secret: ROHOPAY_WEBHOOK_SECRET env var (from Dashboard → Webhooks) Verification: HMAC-SHA256(secret, raw_body) then timing-safe compare Events: deposit.successful — collection or card payment confirmed withdraw.successful — disbursement or withdrawal succeeded withdraw.failed — disbursement or withdrawal failed Payload fields: event, id, internal_reference, provider_reference, type, status, payment_method, phone_number, amount, currency, commission_amount, net_amount, environment, created_at, updated_at ## Commission Default rate: 1% (source: apps/api/internal/services/payment.go) net_amount = amount - commission_amount ## Test Data Test phone (always succeeds, no real USSD): 256700000000 Test cards (use with test_ key only): Visa success: 4111 1111 1111 1111 Visa decline: 4000 0000 0000 0002 MC success: 5500 0000 0000 0004 MC decline: 5200 8282 8210 0001 CVV: any 3 digits | Expiry: any future MM/YY ## Rate Limits (per IP, rolling window) /api/v1/* (collect, disburse, transactions, balance): 100 req/min /api/v1/checkout: 30 req/min POST /api/v2/public/checkout/:slug/pay: 10 req/min GET /api/v2/public/checkout/:slug: 30 req/min /dashboard/transfers: 20 req/min /auth/user/login: 5 req/min ## Error Format { success: false, error: { code, message, reference? } } ## Error Codes UNAUTHORIZED — bad/missing API key MISSING_IDEMPOTENCY_KEY — add Idempotency-Key: {uuid} header VALIDATION_ERROR — bad field value (check message) CARD_EXPIRED — card_expiry is in the past DISBURSE_TEST_BLOCKED — disbursements need live_ key INSUFFICIENT_BALANCE — wallet balance too low PROVIDER_LINE_DOWN — retry with backoff (30s/60s/120s) RATE_LIMIT_EXCEEDED — slow down and retry NOT_FOUND — transaction reference not found ## Dashboard Navigation (actual app structure) Sidebar sections: Overview: /dashboard Payments: Wallet | Transactions | Transfers | Card Payments | Payment Links Build: Projects | Documentation | Webhooks Account: Settings | Help | Sign out Admin (admin role only): Users | Analytics | Commissions | Pulse | Sentinel | System Settings API keys are on the PROJECT DETAIL PAGE — not a standalone menu item. Path: Dashboard → Projects → [click project name] → API Keys section ## Dashboard API (session cookie auth) GET /dashboard/wallet wallet + balances POST /dashboard/wallet/topup mobile money top-up POST /dashboard/wallet/card-topup card top-up → returns payment_url POST /dashboard/wallet/withdraw withdraw to mobile money GET /dashboard/stats summary stats GET /dashboard/projects list projects POST /dashboard/projects create project PUT /dashboard/projects/:id update project DELETE /dashboard/projects/:id delete project GET /dashboard/projects/:id/api-keys list project API keys POST /dashboard/projects/:id/api-keys create API key (returns raw key once) DELETE /dashboard/projects/:id/api-keys/:keyID revoke key POST /dashboard/projects/:id/api-keys/:keyID/rotate rotate key (new key, old revoked) GET /dashboard/transactions all transactions POST /dashboard/transactions/:ref/sync force status sync from provider GET /dashboard/webhook-config webhook secret + event list GET /dashboard/wallets/lookup?query={email} find wallet by email for P2P POST /dashboard/transfers create P2P wallet transfer GET /dashboard/transfers list P2P transfers ## Digital Products API (/api/v2/digital/*, session auth) POST/GET /api/v2/digital/products create / list products GET/PUT/DELETE /api/v2/digital/products/:id get / update / delete product POST/GET /api/v2/digital/links create / list payment links GET/PUT/DELETE /api/v2/digital/links/:id get / update / delete link GET /api/v2/digital/links/:id/revenue revenue for a specific link GET /api/v2/digital/orders list all orders GET /api/v2/digital/orders/:id get single order PUT /api/v2/digital/orders/:id/status update order status PUT /api/v2/digital/orders/:id/delivery update delivery content POST /api/v2/digital/upload upload file to cloud storage ## Public Checkout API (/api/v2/public/*, no auth) GET /api/v2/public/checkout/:slug checkout page data (product, price, methods) POST /api/v2/public/checkout/:slug/pay process payment (10 req/min) GET /api/v2/public/checkout/:slug/order/:id poll order status until "paid" Checkout URL: https://products.rohopay.com/checkout/{slug} ## WordPress Plugin API (/api/v2/plugins/*, session auth) POST/GET /api/v2/plugins/connections create / list site connections GET/DELETE /api/v2/plugins/connections/:id get / delete connection PUT /api/v2/plugins/connections/:id/settings update plugin settings POST /api/v2/plugins/connections/:id/regenerate-token new site token GET /api/v2/plugins/transactions plugin transactions GET/PUT /api/v2/plugins/routing provider routing weights ## Reporting API (/api/v2/reports/*, session auth) GET /api/v2/reports/transactions filtered transaction list GET /api/v2/reports/analytics volume, success rate, breakdown GET /api/v2/reports/settlements settlement records GET/POST /api/v2/reports/exports list / create CSV/PDF exports GET /api/v2/reports/reconciliation reconciliation items PUT /api/v2/reports/reconciliation/:id mark reconciled ## Key Implementation Rules 1. card checkout: api_key in BODY not header (intentional for browser forms) 2. disbursements: live_ key ONLY — 403 with test_ key 3. idempotency: required on collect + disburse — use uuid4, same key on retry 4. webhook verify: read raw body FIRST, then verify HMAC, then parse JSON 5. 3DS redirect: use window.location.href NOT iframe (banks block iframe) 6. after 3DS return: poll transactions/:ref — ?status= query param is provisional only 7. phone format: no + prefix, include country code, pattern ^(256|254|255|250)[0-9]{7,12}$ 8. card expiry format: MM/YY — server validates past expiry before provider call