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

output.txtText
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

request.httpHTTP
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

stringRequired

Your RohoPay API key (test or live). Provided in the body to support browser-side checkout forms.

integerRequired

Amount in smallest currency unit (e.g., 10000 for UGX 10,000).

stringRequired

Currency 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.

stringRequired

Cardholder's full name.

stringRequired

Cardholder's email address.

string

Payment description for the merchant dashboard.

stringRequired

URL the user is redirected to if next_action.type is redirect (success or failure).

stringRequired

16-digit card number without spaces or dashes.

stringRequired

Card expiry in MM/YY format (e.g., 10/26).

stringRequired

3-digit CVV / security code on the back of the card.

Example Request

cURLBash
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"
  }'
TypeScriptTypeScript
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;
}
PythonPython
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

response.jsonJSON
{
  "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.

response.jsonJSON
"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"
}
typeMeaningWhat to do
otpBy 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.
redirectThe 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.
pinIssuer 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.
avsIssuer requires address verification fields.Same as pin — recognized, no submission endpoint yet.
noneNo 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:

request.httpHTTP
POST /api/v1/checkout/{reference}/verify-card-otp
Content-Type: application/json
response.jsonJSON
{
  "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):

response.jsonJSON
{
  "success": true,
  "data": {
    "transaction_id": "01j5m6n7p8q9r0s1t2u3vwxy",
    "reference": "RHP-2024-CARD001",
    "status": "successful",
    "amount": 75000,
    "currency": "UGX",
    "next_action": { "type": "none" }
  }
}

Supported Card Brands

BrandPrefixNotes
Visa4Completes via OTP in most cases; redirect only if the issuing bank mandates 3DS
Mastercard51–55 / 2221–2720Completes via OTP in most cases; redirect only if the issuing bank mandates 3DS

Next Steps