Skip to content

Collect payments

This guide walks the full collect flow: mint a session, collect, the optional submit / OTP / 3DS legs, and reconcile via webhook or poll. The same pattern handles every payment method — only the payment_method and the method-specific fields change.

SituationUse
One-shot card / wallet / mobile-money charge in a mobile appFlutter SDK’s PdirectPayCheckout widget
Same flow in an Angular appAngular SDK’s <pdirect-pay-checkout>
Server-driven hosted page/payments/collect-redirect
Backend-only flow (offline orders, retries)Direct HTTP with the API reference
1. backend POST /internal/sessions/create → session_token
2. client POST /payments/collect → transaction_id, status: pending
3. (optional) POST /payments/submit → status: processing or approved
4. (optional) POST /payments/verify-otp → status: approved
OR
4. (3DS) redirect → 3DS → /card/3ds-callback → status: approved | declined
5. webhook POST notify_url → final state
OR
5. poll GET /payments/status/{transaction_id}

Steps 3 and 4 don’t apply to every method:

Payment methodNeed step 3?Need step 4?
walletYes (PIN)Sometimes (OTP)
mobile_moneyYes (MSISDN)No (push to mobile)
card (HOSTED_SESSION)Yes (session_id)3DS challenge if issuer requires
flashYes (PIN)Sometimes
bank_transferYesNo (manual proof upload)
google_pay / apple_payNo (token at /collect)No

Server-side. The session binds (amount, currency, customer_reference).

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create \
-X POST \
-H "Authorization: Bearer mch_3f9a2e1b:sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c2e9a1d-4b6f-4c8e-9a5f-1d3e2c7b9a4d" \
-d '{
"amount": "12.50",
"currency": "usd",
"customer_reference": "cust_abc123",
"ttl_seconds": 1800
}'
Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/collect \
-X POST \
-H "Authorization: Bearer sess_AbCdEf..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8d3f0e2b-5c7d-4e9f-aab2-2e4f3d8c0b6e" \
-d '{
"payment_method": "wallet",
"user_info": "full",
"payment_method_info": "full",
"fee_covered_by": "buyer",
"delivery_behaviour": "direct_delivery",
"notify_url": "https://merchant.example.com/pdirect/webhook",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "+243998857000",
"wallet_number": "3005710720108954",
"wallet_pin": "048698"
}'

amount, currency, and customer_reference are bound to the session — omit them. They’re still accepted for backwards compat, soft-checked against the binding; drift returns 400.

Response:

{
"success": true,
"transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d",
"status": "pending",
"message": "Payment request received",
"amount": "12.50",
"currency": "usd",
"fee_amount": "0.50",
"total_amount": "13.00",
"created_at": "2026-05-05T10:15:00Z"
}

The SDK widgets handle steps 3–4 internally. If you’re driving the flow with cURL or direct service calls, continue below.

For wallet, flash, mobile money, and card methods you’ll need to submit the method-specific details after /collect:

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/submit \
-X POST \
-H "Authorization: Bearer sess_AbCdEf..." \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_encrypted_abc123def456",
"payment_method_id": "pm_wallet_001",
"ewallet_number": "3005710720108954",
"ewallet_pin": "048698"
}'

The payment_id (encrypted) comes from the /collect response. After a successful /submit the session is CONSUMED — retries need a new session.

Some flows return status: "pending_otp_verification" after submit. Prompt the customer for their six-digit code, then:

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/verify-otp \
-X POST \
-H "Authorization: Bearer sess_AbCdEf..." \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_encrypted_abc123def456",
"otp_code": "482915"
}'

5 / 5min rate limit on this endpoint — see Handle OTP flows.

Two options. Use webhooks in production; polling is fine for development.

POST /pdirect/webhook HTTP/1.1
Host: merchant.example.com
Content-Type: application/json
{
"transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d",
"customer_reference": "cust_abc123",
"status": "approved",
"amount": "12.50",
"currency": "usd",
"completed_at": "2026-05-05T10:15:08Z"
}

See Webhooks overview.

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/status/txn_8f3a4c2e9b1d7a6f5c0e8d \
-H "Authorization: Bearer sess_AbCdEf..."

Use the same session you minted in step 1; it’s still valid until /submit succeeds (which marks it CONSUMED) or it expires.

error_codeMeaningFix
1101 INVALID_AMOUNTBody’s amount doesn’t match session bindingMint a new session
1102 INVALID_CURRENCYBody’s currency doesn’t matchMint a new session
1109 MISSING_REQUIRED_FIELDA required field for the chosen payment_method is missingSee Payment methods
1201 INSUFFICIENT_FUNDSCustomer can’t paySurface to customer
1301 RATE_LIMIT_EXCEEDEDToo many requestsHonour Retry-After
1502 PAYMENT_DECLINEDProcessor rejectedDon’t auto-retry

Full table at Errors → catalogue.