Card Payments Overview

Accept Visa and Mastercard via a hosted checkout redirect.

Overview

RohoPay accepts Visa and Mastercard through a hosted checkout redirect. There is no inline/OTP completion step and RohoPay never receives or stores the card number that is actually charged — you call POST /api/v1/checkout, get back a payment_url, and send the customer's browser there. They enter their card on that page; a webhook tells you the outcome.

💡

card_number, card_expiry, and card_cvv are still required fields on the request and are format-validated server-side (16 digits, not expired, 3-digit CVV) — but they are not the card that gets charged. The customer enters their real card details on the hosted page you redirect them to. Treat these fields as a client-side sanity check, not as PCI-scope data you need to protect end-to-end; RohoPay never forwards them anywhere.

⚠️

Card payments currently only support UGX. RohoPay's card processor (Iotec, hosted PegPay checkout) settles into a single UGX wallet — a request in any other currency is rejected with 400 UNSUPPORTED_CURRENCY before it's sent anywhere, rather than being silently charged in the wrong currency. USD and other currencies are on the roadmap and require the processor to provision a currency-specific wallet first. Mobile money (/api/v1/collect) is not affected — it already supports multiple currencies.

How Card Payments Work

output.txtText
1. Your app calls POST /api/v1/checkout with amount + customer details
2. RohoPay creates a pending order and returns a payment_url
3. Redirect the customer's browser to payment_url
4. The customer enters their card and completes payment on the hosted page
5. They're redirected back to your return_url (provisional UX hint only)
6. RohoPay's webhook confirms the definitive status and credits your wallet

See Return Flow below for what happens at step 5, and Webhooks for step 6.

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

Whole currency units. Minimum UGX 2,000 for card.

stringRequired

Must be UGX — this is currently the only currency card payments support. RohoPay's card processor settles into a UGX-only wallet and silently ignores a mismatched currency rather than erroring, so RohoPay rejects anything other than UGX with 400 UNSUPPORTED_CURRENCY before the request ever reaches the processor. Other currencies are on the roadmap — see the note below.

string

Which hosted card processor to use. Currently only iotec is available (also the default when omitted) — any other value is rejected with 400 INVALID_REQUEST. Present for forward compatibility as additional processors come online.

stringRequired

Cardholder's full name.

stringRequired

Cardholder's email address.

string

Payment description for the merchant dashboard.

stringRequired

Where the customer's browser is redirected after they finish on the hosted page (see Return Flow).

stringRequired

16-digit card number without spaces or dashes. Format-validated only — see the note above.

stringRequired

Card expiry in MM/YY format (e.g., 10/26). Rejected server-side if already expired.

stringRequired

3-digit CVV / security code.

string

Dedupes a retried checkout initiation. See Idempotency.

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();

// Full browser redirect — there is no inline/embedded completion step.
window.location.href = data.payment_url;
PythonPython
import httpx, os

def initiate_card_payment(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": "5531886652142950",
            "card_expiry": "09/32",
            "card_cvv": "564",
        },
    )
    res.raise_for_status()
    return res.json()["data"]  # redirect the browser to data["payment_url"]

Response (200 OK)

response.jsonJSON
{
  "success": true,
  "data": {
    "transaction_id": "01j5m6n7p8q9r0s1t2u3vwxy",
    "reference": "ref_card001",
    "status": "pending",
    "amount": 75000,
    "currency": "UGX",
    "commission": 750,
    "provider": "rohopay",
    "net_amount": 74250,
    "payment_url": "https://checkout.example/pay/abc123"
  },
  "message": "Checkout initiated"
}

Redirect the customer's browser to payment_url immediately — there is no other action to take on this response.

Return Flow

After the customer finishes (or abandons) the hosted page, they land on your return_url with query parameters:

output.txtText
https://your-app.com/payment/return?reference=ref_card001&status=successful
⚠️

status in the query string is a provisional UX hint only. It reflects what the hosted page told the browser during the redirect — never fulfil an order from it. Always wait for the webhook or poll GET /api/v1/transactions/{reference} for the definitive result.

index.tsTypeScript
// In your /payment/return route
export default async function CheckoutReturn({ searchParams }) {
  const ref = searchParams.reference;
  if (!ref) return <ErrorPage />;

  // Poll until the webhook has confirmed the real status.
  let status = "pending";
  for (let i = 0; i < 12 && status === "pending"; i++) {
    await new Promise(r => setTimeout(r, 2500));
    const res = await fetch(`https://api.rohopay.com/api/v1/transactions/${ref}`, {
      headers: { Authorization: `Bearer ${process.env.ROHOPAY_API_KEY}` },
    });
    status = (await res.json()).data.status;
  }

  return status === "successful" ? <SuccessPage /> : <FailurePage />;
}

Common Issues

Customer closes the browser before completing payment

The transaction stays at pending and eventually expires on the provider side. No charge occurs. Show an "In Progress" state with a way to retry — call checkout again with a new idempotency_key.

Return page shows success but the webhook says failed

Trust the webhook. Redirect query parameters can be manipulated or intercepted. The HMAC-verified webhook is the authoritative source.

Do I need to validate the card fields myself before sending?

RohoPay already rejects a malformed number, an invalid MM/YY, or an expired card with 400 INVALID_CARD before creating the checkout session — you don't need to duplicate that check, though basic client-side validation still improves UX by failing fast.

Getting 400 UNSUPPORTED_CURRENCY

Card payments only accept currency: "UGX" right now (see the warning above). Charge in UGX, or use mobile money if you need another currency today.

Supported Card Brands

BrandPrefix
Visa4
Mastercard51–55 / 2221–2720

Next Steps