Retry a failed payment
Payment failures happen—networks timeout, customers enter wrong PINs, or mobile money accounts have insufficient balance. Instead of losing the sale, give your customers an easy way to retry. This guide shows you three strategies for recovering from failed payments, each optimized for different scenarios.
How payment retries work
When a payment fails, the order remains in the system with all its details intact. You use the /orders/pay endpoint to retry payment without creating a new order. This preserves the order ID, line items, and customer information while giving you flexibility in how to process the payment. You can retry with the same payment method (network glitch scenarios), switch to a different saved payment method (customer preference), or collect entirely new payment details (expired card, wrong account). Each retry triggers a fresh OTP and payment confirmation flow.
Payment retries are safe and idempotent. The order total remains unchanged, and each retry creates a new payment attempt that's tracked separately. Your customer won't be double-charged.
Scenario 1: Retry with the current payment method
Use this approach when the failure was likely temporary—a network timeout, momentary connectivity issue, or the customer accidentally dismissed the OTP prompt. The payment method itself is still valid; it just needs another attempt.
When you call /orders/pay with only the order_id, Commerce automatically retries the payment using the payment method already attached to the order. This is the simplest retry flow and works great for transient failures.
Retry with current method
curl https://api.zebo.dev/orders/pay \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "ord_failed_123"
}'
- Name
order_id- Type
- string
- Description
The ID of the order with the failed payment. Find this in your original order creation response or webhook payload.
The response includes a new OTP that was sent to the customer's phone. Present your OTP confirmation UI and call /orders/confirm_payment when the customer enters the code. The payment flow is identical to the original attempt—only the payment attempt changes.
When retrying with the current payment method, Commerce creates a new payment attempt but keeps the same payment intent. This means your webhook will show multiple attempts for the same payment, which is normal and expected.
Scenario 2: Retry with a different saved payment method
Use this when the customer wants to switch to a different payment method they've previously used. Maybe their primary mobile money account has insufficient funds, or they prefer to use a different network this time. You'll pass the payment_method_id of an alternative saved method.
This only works for returning customers who have multiple saved payment methods. You can get available payment method IDs from the customer's previous orders or by calling the customer endpoint (if you've implemented customer management).
Retry with saved method
curl https://api.zebo.dev/orders/pay \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "ord_failed_123",
"payment_method_id": "pm_alternative_method"
}'
- Name
order_id- Type
- string
- Description
The ID of the order with the failed payment.
- Name
payment_method_id- Type
- string
- Description
The ID of an alternative saved payment method to use for this retry. The payment method must belong to the same customer as the original order.
The payment method must belong to the customer who owns the order. Commerce validates this automatically and returns an error if there's a mismatch. This prevents accidental or malicious payment method substitution.
Scenario 3: Retry with new payment details
Use this when the customer needs to provide completely new payment information—their old card expired, they closed their mobile money account, or they want to pay with a method they've never used before. You'll pass full payment details via payment_method_data, just like creating a new order.
This is the most flexible retry option. It works whether the customer is new or returning, and it automatically saves the new payment method for future use.
Retry with new details
curl https://api.zebo.dev/orders/pay \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "ord_failed_123",
"payment_method_data": {
"type": "mobile_money",
"mobile_money": {
"issuer": "vodafone",
"number": "0209876543"
}
}
}'
- Name
order_id- Type
- string
- Description
The ID of the order with the failed payment.
- Name
payment_method_data- Type
- object
- Description
Complete payment method details. Structured exactly like the
payment_method_datayou'd provide when creating a new order.- Name
type- Type
- string
- Description
Payment method type. Currently only
mobile_moneyis supported.
- Name
mobile_money- Type
- object
- Description
Mobile money payment details (required when type is
mobile_money).- Name
issuer- Type
- string
- Description
Mobile money provider:
mtn,vodafone, orairteltigo.
- Name
number- Type
- string
- Description
Mobile money account number (typically a phone number). Should match the number that will receive the OTP.
When you provide new payment details, Commerce automatically saves the payment method and associates it with the customer. The customer can use this method for future purchases without re-entering it.
Confirming the retry
Regardless of which retry method you use, the payment confirmation flow is identical. After calling /orders/pay, Commerce sends a 6-digit OTP to the customer's phone. Collect this code through your UI and call /orders/confirm_payment to complete the payment.
// After any retry method
async function confirmRetry(orderId: string, token: string) {
const result = await commerce.orders.confirmPayment({
order_id: orderId,
token: token
})
if (result.order.status === 'completed') {
console.log('Payment retry succeeded!')
}
return result
}
See the Accept a payment guide for detailed information on the confirmation flow, including handling timeouts, resending OTPs, and processing webhooks.
Best practices
Let customers choose their retry strategy
Don't automatically retry with the same method. Show a failure message that explains what happened and give customers options: "Try again with this account", "Use a different saved account", or "Pay with a new account". This respects customer preference and improves success rates.
Handle common failure patterns
Track why payments fail and adjust your retry UI accordingly. If the failure was insufficient_funds, suggest an alternative payment method immediately. If it was network_timeout, auto-retry once with the same method before asking the customer. If it was invalid_pin, explain that they need to enter the correct PIN—retrying won't help until they fix their input.
Preserve order context
When retrying, show the customer what they're paying for. Display the order summary, line items, and total prominently so they can verify everything before confirming the OTP. This prevents confusion about duplicate charges.
Set retry limits
Don't allow unlimited retries. After 3-5 failed attempts, pause and suggest the customer contact their mobile money provider or try a different payment method entirely. This prevents fraud patterns and protects against brute-force attacks.
Monitor retry success rates
Track which retry strategies work best for your customers. If retries with saved methods succeed more often than new payment details, optimize your UI to suggest saved methods first. Use this data to improve your payment flow over time.
Never expose payment method IDs in client-side code or URLs. Always fetch them server-side and validate ownership before allowing retries. Treat payment method IDs with the same security as you would credit card numbers.