Card Payments Overview
Accept Visa and Mastercard across East Africa via RohoPay's card processing network, with in-app OTP as the common completion step.
Overview
RohoPay supports Visa and Mastercard payments through RohoPay's card processing network. Most cards complete via a one-time code (OTP) sent to the cardholder's phone or email — no redirect required. A full 3D Secure redirect only happens when the issuing bank mandates it.
RohoPay's Go backend performs the required encryption server-side before forwarding your request — you send the same plain request body you always have (api_key, card_number, card_expiry, card_cvv, etc.), with no client-side encryption of your own.
How Card Payments Work
1. Your app calls POST /api/v1/checkout with card details + return_url
2. RohoPay creates a card order and returns a next_action object
3. Branch on next_action.type:
- "otp" → show an inline code input, submit to verify-card-otp
- "redirect" → send the user to next_action.redirect_url (bank-mandated 3DS)
- "none" → no further action; poll for the final status
4. Provider webhook confirms status (HMAC-verified)
5. RohoPay confirms status and credits the wallet
Endpoint
POST /api/v1/checkout
Content-Type: application/json
Unlike mobile money endpoints, the card checkout endpoint authenticates via api_key in the request body rather than the Authorization header. This allows client-side checkout flows.
Request Body
stringRequiredYour RohoPay API key (test or live). Provided in the body to support browser-side checkout forms.
integerRequiredAmount in smallest currency unit (e.g., 10000 for UGX 10,000).
stringRequiredCurrency code (e.g. UGX, USD, KES, NGN). Currency is request-driven; the API no longer rejects non-UGX currencies. Mobile-money charge routing currently defaults to Uganda — widening to other countries depends on the provider country table.
stringRequiredCardholder's full name.
stringRequiredCardholder's email address.
stringPayment description for the merchant dashboard.
stringRequiredURL the user is redirected to if next_action.type is redirect (success or failure).
stringRequired16-digit card number without spaces or dashes.
stringRequiredCard expiry in MM/YY format (e.g., 10/26).
stringRequired3-digit CVV / security code on the back of the card.
Example Request
curl -X POST https://api.rohopay.com/api/v1/checkout \
-H "Content-Type: application/json" \
-d '{
"api_key": "live_YOUR_KEY",
"amount": 75000,
"currency": "UGX",
"customer_name": "Jane Mukasa",
"customer_email": "jane@example.com",
"description": "Premium subscription",
"return_url": "https://your-app.com/payment/return",
"card_number": "5531886652142950",
"card_expiry": "09/32",
"card_cvv": "564"
}'
const res = await fetch("https://api.rohopay.com/api/v1/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.ROHOPAY_API_KEY,
amount: 75000,
currency: "UGX",
customer_name: "Jane Mukasa",
customer_email: "jane@example.com",
description: "Premium subscription",
return_url: "https://your-app.com/payment/return",
card_number: "5531886652142950",
card_expiry: "09/32",
card_cvv: "564",
}),
});
const { data } = await res.json();
// Always branch on next_action.type — never on whether a field is present.
switch (data.next_action.type) {
case "otp":
// Show an inline one-time-code input, then POST it to
// /api/v1/checkout/{reference}/verify-card-otp
break;
case "redirect":
window.location.href = data.next_action.redirect_url;
break;
case "none":
// Poll the transaction until the webhook confirms the final status.
break;
}
import httpx, os
def initiate_card_payment(card_number: str, expiry: str, cvv: str, amount: int) -> dict:
res = httpx.post(
"https://api.rohopay.com/api/v1/checkout",
json={
"api_key": os.environ["ROHOPAY_API_KEY"],
"amount": amount,
"currency": "UGX",
"customer_name": "Jane Mukasa",
"customer_email": "jane@example.com",
"return_url": "https://your-app.com/payment/return",
"card_number": card_number,
"card_expiry": expiry,
"card_cvv": cvv,
},
)
res.raise_for_status()
data = res.json()["data"]
return data # branch on data["next_action"]["type"]
Successful Response
{
"success": true,
"data": {
"transaction_id": "01j5m6n7p8q9r0s1t2u3vwxy",
"reference": "RHP-2024-CARD001",
"status": "pending",
"amount": 75000,
"currency": "UGX",
"commission": 750,
"net_amount": 74250,
"next_action": {
"type": "otp",
"flw_ref": "FLW-REF-abc123xyz"
}
}
}
The next_action Contract
Every card checkout response — and now also some mobile-money collection responses, since a subset of RohoPay's mobile money rails are redirect-based — includes a next_action object. Always branch on next_action.type, never on whether a field happens to be present.
"next_action": {
"type": "pin" | "otp" | "avs" | "redirect" | "none",
"redirect_url": "...", // present only when type == "redirect"
"flw_ref": "...", // present when type == "otp" (needed to submit the OTP)
"fields": ["..."] // present only when type == "avs"
}
type | Meaning | What to do |
|---|---|---|
otp | By far the most common case. The card issuer sent a one-time code by SMS or email. | Show an inline code input in your UI and submit it to POST /api/v1/checkout/{reference}/verify-card-otp. No redirect, no leaving your page. |
redirect | The rare case where the issuing bank mandates full 3D Secure. | Send the cardholder to next_action.redirect_url — present it as an in-app overlay/iframe where possible, with a full page redirect as an acceptable fallback. See 3D Secure Flow. |
pin | Issuer requires the card PIN. | Recognized in the response, but RohoPay's public API does not yet expose a submission endpoint for this — only otp and redirect have a defined completion flow today. |
avs | Issuer requires address verification fields. | Same as pin — recognized, no submission endpoint yet. |
none | No further cardholder action needed. | Poll the transaction status (or wait for your webhook) until it resolves to a final state. |
Submitting the OTP
When next_action.type is otp, collect the code from the cardholder and submit it:
POST /api/v1/checkout/{reference}/verify-card-otp
Content-Type: application/json
{
"otp": "12345"
}
No separate auth header is required — the reference itself is a high-entropy generated ID, the same trust model as the rest of the checkout flow.
The response has the same shape as the checkout response (transaction_id, reference, status, amount, next_action):
{
"success": true,
"data": {
"transaction_id": "01j5m6n7p8q9r0s1t2u3vwxy",
"reference": "RHP-2024-CARD001",
"status": "successful",
"amount": 75000,
"currency": "UGX",
"next_action": { "type": "none" }
}
}
Supported Card Brands
| Brand | Prefix | Notes |
|---|---|---|
| Visa | 4 | Completes via OTP in most cases; redirect only if the issuing bank mandates 3DS |
| Mastercard | 51–55 / 2221–2720 | Completes via OTP in most cases; redirect only if the issuing bank mandates 3DS |
