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.
When OTP fires
Section titled “When OTP fires”| Method | OTP triggered? |
|---|---|
wallet | Sometimes — depends on amount, customer history, and merchant policy |
flash | Sometimes |
mobile_money | No — the customer authorises in-app on their carrier UI |
card | No (3DS replaces OTP — different flow) |
bank_transfer | No |
If the /submit response includes
status: "pending_otp_verification", the customer should expect a
six-digit code via SMS or email within seconds.
Verify
Section titled “Verify”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.
Resend
Section titled “Resend”The customer didn’t get the SMS, or the OTP expired. Call
/payments/resend-otp:
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.
Rate limits
Section titled “Rate limits”The Retry-After header is set on 429. Honour it.
SDK behaviour
Section titled “SDK behaviour”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.
Common failures
Section titled “Common failures”error_code | Meaning | Fix |
|---|---|---|
1108 INVALID_CARD_CVV | Used as the validation code for OTP format too — not exactly 6 digits | Validate before sending |
1203 DUPLICATE_TRANSACTION | OTP already verified for this payment_id | Stop. The transaction succeeded. |
1301 RATE_LIMIT_EXCEEDED | 5/5min limit hit | Show “wait, try again” |
1001 INVALID_APP_KEY | Session expired or consumed | Mint a new session |
Customer experience
Section titled “Customer experience”Two patterns work in practice:
- Numeric pad with auto-advance. Six segments, paste-friendly from SMS auto-fill. The OTP code field on both SDKs uses this pattern.
- Single text input with
inputmode="numeric"andautocomplete="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.
See also
Section titled “See also”- Payment methods — which methods trigger OTP
- Rate limits — the tight buckets explained
- API reference: OTP