Skip to content

Handle OTP flows

Some payment methods require a one-time password to authorise the transaction. The flow is always the same: /submit returns status: "pending_otp_verification", you collect the six-digit code from the customer, and call /payments/verify-otp.

MethodOTP triggered?
walletSometimes — depends on amount, customer history, and merchant policy
flashSometimes
mobile_moneyNo — the customer authorises in-app on their carrier UI
cardNo (3DS replaces OTP — different flow)
bank_transferNo

If the /submit response includes status: "pending_otp_verification", the customer should expect a six-digit code via SMS or email within seconds.

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"
}'

The payment_id is the encrypted ID returned from /submit. The otp_code is exactly six digits.

Response on success:

{
"success": true,
"payment_id": "pay_encrypted_abc123def456",
"status": "approved",
"message": "OTP verified successfully",
"debit_amount": 12.50,
"wallet_balance": 87.50,
"verified_at": "2026-05-05T10:17:30Z",
"completed_at": "2026-05-05T10:17:35Z"
}

The session is now consumed. Reconcile via webhook or status query.

The customer didn’t get the SMS, or the OTP expired. Call /payments/resend-otp:

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

Response:

{
"success": true,
"payment_id": "pay_encrypted_abc123def456",
"status": "pending_otp_verification",
"message": "OTP resent successfully",
"otp_sent_to": "j***@example.com",
"resend_count": 1,
"max_resends": 3,
"resent_at": "2026-05-05T10:18:00Z",
"expires_at": "2026-05-05T10:28:00Z"
}

max_resends defaults to 3. Beyond that, /resend-otp returns error_code: "1301" with Retry-After.

The Retry-After header is set on 429. Honour it.

Both SDKs call /verify-otp and /resend-otp internally when the drop-in widget detects status: "pending_otp_verification" after /submit. The widget shows a six-digit input and a “Resend code” button.

If you’re driving the flow yourself (via PdirectPaymentService or PdirectHttpClient), you handle the OTP UI.

error_codeMeaningFix
1108 INVALID_CARD_CVVUsed as the validation code for OTP format too — not exactly 6 digitsValidate before sending
1203 DUPLICATE_TRANSACTIONOTP already verified for this payment_idStop. The transaction succeeded.
1301 RATE_LIMIT_EXCEEDED5/5min limit hitShow “wait, try again”
1001 INVALID_APP_KEYSession expired or consumedMint a new session

Two patterns work in practice:

  1. Numeric pad with auto-advance. Six segments, paste-friendly from SMS auto-fill. The OTP code field on both SDKs uses this pattern.
  2. Single text input with inputmode="numeric" and autocomplete="one-time-code". Lets iOS / Android suggest the code from the SMS automatically.

Resend should be visible only after a short delay (e.g. 30 s) so customers don’t ask for a fresh code before the first one’s even arrived.