Why Polling Is Needed

Mobile money payments are asynchronous. After calling POST /api/v1/collect, the transaction sits at pending until the user approves (or rejects) the USSD prompt on their phone. This can take 5 seconds to 5 minutes. Recommended approach: Use webhooks in production. Use polling for quick integrations, dashboards, or when a callback URL is not available.

Get a Single Transaction

GET /api/v1/transactions/{reference}
Authorization: Bearer {api_key}
The reference can be either:
  • internal_reference (e.g., RHP-2024-ABC123) — returned in the collect response
  • provider_reference — the ID from the payment provider

Response

{
  "success": true,
  "data": {
    "id": "01j2k3m4n5p6q7r8s9t0uvwx",
    "internal_reference": "RHP-2024-ABC123",
    "provider_reference": "9876543210",
    "status": "successful",
    "payment_method": "mobile_money",
    "provider": "relworx",
    "phone_number": "256700123456",
    "amount": 50000,
    "currency": "UGX",
    "net_amount": 49000,
    "commission_amount": 1000,
    "environment": "live",
    "created_at": "2024-07-15T08:30:00Z",
    "updated_at": "2024-07-15T08:31:47Z"
  }
}

List Transactions

GET /api/v1/transactions
Authorization: Bearer {api_key}
Returns all transactions for the authenticated project, sorted by created_at descending.

Polling Pattern

Implement polling with exponential backoff to avoid hitting rate limits:
type TransactionStatus = "pending" | "successful" | "failed";

interface Transaction {
  internal_reference: string;
  status: TransactionStatus;
  amount: number;
  currency: string;
}

async function pollUntilComplete(
  ref: string,
  apiKey: string,
  maxAttempts = 24,
  intervalMs = 5000
): Promise<Transaction> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    if (attempt > 0) {
      await new Promise(resolve => setTimeout(resolve, intervalMs));
    }

    const res = await fetch(`https://api.rohopay.com/api/v1/transactions/${ref}`, {
      headers: { "Authorization": `Bearer ${apiKey}` },
    });

    const { success, data, error } = await res.json();

    if (!success) throw new Error(error?.message ?? "Fetch failed");
    if (data.status === "successful") return data;
    if (data.status === "failed") throw new Error("Payment was rejected or failed");
  }

  throw new Error(`Payment still pending after ${maxAttempts} attempts`);
}

Payment States

StatusMeaningTerminal?
pendingAwaiting user USSD approvalNo
successfulUser approved; funds receivedYes ✅
failedDeclined, expired, or rejectedYes ❌
Once a transaction reaches successful or failed it will never change again.
Typical USSD approval takes 10–60 seconds. Poll every 5 seconds for at most 2 minutes before treating the payment as timed out on your side.