Skip to content

Mobile money status polling

Mobile money settlements are always asynchronous. The customer receives a USSD or in-app prompt; the gateway only knows the result once the carrier confirms.

Use GET /payments/mobile-money/status/{payment_id} to poll. The endpoint is optimised for live UI: it returns a progress percentage, a localised message, and an estimated_completion timestamp.

After /payments/submit returns status: "pending_mobile_money_verification" (or simply processing):

1. POST /payments/collect → status: pending
2. POST /payments/submit → status: processing ← start polling
{ mobile_money_phone_number: "..." }
3. GET /payments/mobile-money/status/... → status: processing, progress: 25
4. GET /payments/mobile-money/status/... → status: processing, progress: 50
5. GET /payments/mobile-money/status/... → status: completed, progress: 100

In production, prefer the webhook on notify_url over polling. Polling is for the UI spinner — webhooks are authoritative.

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/mobile-money/status/pay_encrypted_abc123 \
-H "Authorization: Bearer sess_..."

Response:

{
"status_code": 200,
"message": "Status retrieved successfully",
"data": {
"status": "processing",
"progress": 50,
"message": "Payment is being processed by the carrier",
"payment_info": null,
"provider_reference": "onafriq_ref_abc123",
"estimated_completion": "2026-05-05T10:17:00Z"
}
}

Fields:

FieldNotes
statusOne of pending, processing, completed, failed, cancelled.
progress0100. Approximate. Use it to drive a progress bar.
messageLocalised status. Suitable for “Processing your payment” copy.
payment_infoPopulated only on terminal states (completed, failed).
provider_referenceCarrier-side ID. Quote when contacting support.
estimated_completionWhen the carrier expects to be done. Use to set polling cadence.

Poll every 5 seconds with a maximum of 60 attempts (≈ 5 minutes total). That’s what both SDKs do internally.

async function pollUntilTerminal(paymentId: string, sessionToken: string) {
for (let i = 0; i < 60; i++) {
const r = await fetch(
`https://app.api.gtwy.pdirect.com/api/v1/payments/mobile-money/status/${paymentId}`,
{ headers: { Authorization: `Bearer ${sessionToken}` } },
);
const { data } = await r.json();
if (data.status === "completed" || data.status === "failed" || data.status === "cancelled") {
return data;
}
await new Promise((r) => setTimeout(r, 5000));
}
return { status: "timeout" } as const;
}

For very-long-tail flows (some carriers in some markets can take 30+ minutes), surrender polling and rely on the webhook for the final state.

The Flutter SDK’s PdirectMobileMoneyProgressScreen polls every 5 seconds, max 60 attempts, drives the progress UI, and dispatches into the PdirectCommonService.paymentScreenStream when terminal.

The Angular SDK’s <pdirect-payment-status> component does the same, configurable via PdirectPaymentStatusConfig:

const statusConfig: PdirectPaymentStatusConfig = {
paymentId: "pay_encrypted_abc123",
config: { token: "merchant-app-key", isProduction: true },
pollingInterval: 5000, // ms
maxPollingAttempts: 60,
autoStart: true,
};

If the local transaction is already in a terminal state (e.g. you’ve just received the webhook), the gateway returns immediately without calling the carrier — the local DB is the source of truth once the state is final.

error_codeMeaningFix
1110 INVALID_FIELD_FORMATpayment_id < 10 charsPass the full encrypted ID
1204 TRANSACTION_NOT_FOUNDWrong session or wrong IDCheck payment_id, ensure session is for the same merchant
1404 EXTERNAL_SERVICE_ERRORCarrier API downBack off, retry