Order now, pay later
Sometimes you need to create an order before collecting payment—maybe you're confirming inventory, waiting for manager approval, or building a pre-order system. This guide shows you how to decouple order creation from payment execution, giving you complete control over when and how you charge customers.
How it works
The standard payment flow combines order creation and payment into one step, but Commerce lets you split them apart. First, create an order with execute_payment: false (or omit it entirely) to capture the customer's intent without charging them. Later, when you're ready, call /orders/pay to initiate the charge, then confirm it with an OTP as usual. This pattern gives you flexibility while maintaining a complete audit trail from order creation through final payment.
Step 1: Create the order without payment
Create the order exactly as you would for immediate payment, but set execute_payment: false or leave out payment method details entirely. Commerce creates the order, assigns it an ID, and saves all the details—but doesn't contact the payment provider yet. The order status starts at preparing, waiting for you to execute payment later.
Create order
curl https://api.zebo.dev/orders/new \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "order_2025_001",
"execute_payment": false,
"customer_data": {
"name": "Gloria Kesewaa",
"email_address": "gloria@example.com",
"phone_number": "+233544998605"
},
"line_items": [
{
"type": "product",
"product": {
"type": "physical",
"name": "Custom Furniture",
"quantity": 1,
"price": { "currency": "ghs", "value": 150000 }
}
}
]
}'
Response
This is a partial response showing key attributes. See the complete Order object for all available fields.
{
"order": {
"id": "cPxZ9kL2mQrTfVqYz1wNbMxKs8dGhJ4p",
"status": "preparing",
"customer": {
"id": "cus_abc123",
"name": "Gloria Kesewaa",
"email_address": "gloria@example.com"
},
"line_item_group": {
"total": {
"currency": "ghs",
"value": 150000
}
}
}
}
Key attributes:
order.id- Save this—you'll use it to execute payment laterstatus: "preparing"- Order exists but no payment initiated yetpaymentobject attached
Step 2: Execute payment when ready
When you're ready to collect payment—after confirming inventory, receiving approval, or whenever your business logic dictates—call /orders/pay with the order ID and payment method details. Commerce contacts the mobile money provider, initiates the charge, and sends an OTP to the customer just like in the standard flow. You can provide new payment method details or reference a saved payment method if the customer has one.
Execute payment
curl https://api.zebo.dev/orders/pay \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "cPxZ9kL2mQrTfVqYz1wNbMxKs8dGhJ4p",
"payment_method_data": {
"type": "mobile_money",
"mobile_money": {
"issuer": "mtn",
"number": "0544998605"
}
}
}'
Response
This is a partial response showing key attributes. See the complete Order object for all available fields.
{
"order": {
"id": "cPxZ9kL2mQrTfVqYz1wNbMxKs8dGhJ4p",
"status": "requires_payment",
"payment": {
"id": "pay_xyz789",
"status": "requires_action",
"next_action": {
"type": "confirm_payment",
"confirm_payment": {
"expires_at": "2025-01-13T10:08:00Z",
"request": {
"recipient": "0544998605",
"sent_via": "sms"
}
}
}
}
}
}
What changed:
- Now includes
paymentobject with payment intent details payment.status: "requires_action"- OTP sent to customer- Order status still
requires_paymentuntil OTP is confirmed
Step 3: Confirm payment with OTP
From here, the flow is identical to standard payments. The customer receives an OTP via SMS, enters it in your app, and you submit it to confirm the payment. Commerce verifies the code, charges the mobile money account, and moves the order to paid status.
Confirm payment
curl https://api.zebo.dev/orders/confirm_payment \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "cPxZ9kL2mQrTfVqYz1wNbMxKs8dGhJ4p",
"token": "302673"
}'
Common use cases
Pre-orders with inventory confirmation
Create orders as customers place them, but only charge after confirming you have stock. This prevents overselling and charging customers for items you can't deliver.
// Customer places pre-order
const order = await commerce.orders.create({
execute_payment: false,
customer_data: { /* ... */ },
line_items: [
{
type: 'product',
product: {
name: 'Limited Edition Sneakers',
quantity: 1,
price: { currency: 'ghs', value: 50000 },
},
},
],
})
// Store order and check inventory
await db.orders.create({
commerce_order_id: order.id,
status: 'pending_inventory_check',
})
// Later, after confirming stock availability
const inventoryAvailable = await checkInventory('sneakers', 1)
if (inventoryAvailable) {
// Charge the customer
await commerce.orders.pay({
order_id: order.id,
payment_method_data: {
type: 'mobile_money',
mobile_money: { issuer: 'mtn', number: '0544998605' },
},
})
// Customer enters OTP, payment completes
await commerce.orders.confirmPayment({
order_id: order.id,
token: otp,
})
} else {
// Cancel order and notify customer
await notifyCustomer('Out of stock')
}
Manager approval workflows
For B2B or high-value transactions, create the order immediately but require manager approval before charging. The order captures the customer's intent while giving you time for internal review.
// Sales rep creates order
const order = await commerce.orders.create({
execute_payment: false,
customer_id: 'enterprise_customer_xyz',
line_items: [
{
type: 'product',
product: {
name: 'Enterprise Software License',
quantity: 50,
price: { currency: 'ghs', value: 500000 },
},
},
],
})
// Send for approval
await sendForApproval({
order_id: order.id,
amount: 500000,
approver: 'sales_manager@company.com',
})
// Later, when manager approves
if (approved) {
// Use saved payment method
await commerce.orders.pay({
order_id: order.id,
payment_method_id: 'pm_saved_method',
})
// Auto-confirm if using verified payment method
await commerce.orders.confirmPayment({
order_id: order.id,
token: otp,
})
}
Subscription setup before first charge
Set up the subscription order and verify customer details before taking the first payment. This gives customers a chance to review and confirm before any money moves.
// Create subscription order
const order = await commerce.orders.create({
execute_payment: false,
customer_id: 'existing_customer',
line_items: [
{
type: 'product',
product: {
name: 'Monthly Subscription',
quantity: 1,
price: { currency: 'ghs', value: 5000 },
},
},
],
})
// Show customer confirmation screen
await showConfirmation({
plan: 'Monthly Subscription',
amount: 'GHS 50.00',
billing_date: 'Today, then monthly',
next_billing: '2025-02-13',
})
// After customer confirms
if (customerConfirmed) {
await commerce.orders.pay({
order_id: order.id,
payment_method_id: savedPaymentMethod.id,
})
await commerce.orders.confirmPayment({
order_id: order.id,
token: otp,
})
// Schedule recurring payments
await scheduleRecurringBilling(order.id)
}
Layaway and installment plans
Let customers reserve items with an order, then collect payment in installments over time. Each payment can reference the same order or you can create child orders for each installment.
// Create layaway order
const order = await commerce.orders.create({
execute_payment: false,
customer_data: { /* ... */ },
line_items: [
{
type: 'product',
product: {
name: 'Furniture Set',
quantity: 1,
price: { currency: 'ghs', value: 200000 },
},
},
],
})
// Store layaway plan
await db.layawayPlans.create({
order_id: order.id,
total_amount: 200000,
installments: 4,
installment_amount: 50000,
frequency: 'monthly',
})
// Collect first installment
await commerce.orders.pay({
order_id: order.id,
payment_method_data: {
type: 'mobile_money',
mobile_money: { issuer: 'mtn', number: '0544998605' },
},
})
// Note: For installments, you'd typically create separate orders
// for each payment rather than charging the same order multiple times
Handling edge cases
Customer cancels before payment
Orders created without payment can be canceled at any time before payment execution. Simply update your internal records—Commerce doesn't require explicit cancellation for unpaid orders.
// Customer decides not to proceed
await db.orders.update({
commerce_order_id: order.id,
status: 'cancelled_by_customer',
cancelled_at: new Date(),
})
// No need to call Commerce API—order was never charged
Payment method changes between creation and payment
The customer might want to use a different payment method than what was initially discussed. Just pass the new payment method details when calling /orders/pay.
// Order created (no payment method specified)
const order = await commerce.orders.create({
execute_payment: false,
customer_id: 'cus_abc123',
line_items: [ /* ... */ ],
})
// Customer initially chose MTN but wants to use Vodafone
await commerce.orders.pay({
order_id: order.id,
payment_method_data: {
type: 'mobile_money',
mobile_money: {
issuer: 'vodafone', // Different from original choice
number: '0501234567',
},
},
})
Order expires before payment
Set internal expiration policies for unpaid orders to prevent customers from completing very old orders. Check the order age before executing payment and reject if too old.
async function executePayment(orderId: string) {
const order = await db.orders.findOne({ commerce_order_id: orderId })
// Check if order is too old (e.g., 30 days)
const ageInDays = daysSince(order.created_at)
if (ageInDays > 30) {
throw new Error('Order has expired. Please create a new order.')
}
// Proceed with payment
await commerce.orders.pay({
order_id: orderId,
payment_method_data: { /* ... */ },
})
}
Testing
Test the deferred payment flow using the same test numbers as standard payments. The key difference is you'll make two separate calls—one to create the order, then another to execute payment after some delay.
# Step 1: Create order without payment
curl https://api-test.zebo.dev/orders/new \
-H "Authorization: Bearer TEST_KEY" \
-d '{
"execute_payment": false,
"customer_data": {
"name": "Test User",
"phone_number": "+233242000003"
},
"line_items": [...]
}'
# Response includes order.id but no payment object
# Step 2: Execute payment (can be seconds or days later)
curl https://api-test.zebo.dev/orders/pay \
-H "Authorization: Bearer TEST_KEY" \
-d '{
"order_id": "test_order_id",
"payment_method_data": {
"type": "mobile_money",
"mobile_money": { "issuer": "mtn", "number": "0242000003" }
}
}'
# Step 3: Confirm with test OTP
curl https://api-test.zebo.dev/orders/confirm_payment \
-d '{"order_id": "test_order_id", "token": "000000"}'
Key tips
Track order state: Since payment is deferred, maintain clear status tracking in your database. Use states like pending_approval, awaiting_inventory, or ready_for_payment to know which orders are waiting for what.
Set expiration policies: Decide how long orders can remain unpaid before they expire. Common policies: 24 hours for standard orders, 7 days for pre-orders, 30 days for B2B approvals. Check age before executing payment.
Communicate clearly: Tell customers when they'll be charged. "You won't be charged until we confirm availability" or "Payment will be collected after manager approval." Set expectations upfront.
Handle abandoned orders: Not all created orders will result in payment. Set up automated processes to follow up on unpaid orders, remind customers, or clean up very old orders from your database.
Next steps
- Accept a payment - Standard payment flow with immediate charging
- Retry a failed payment - Retry with alternative payment methods
- Charge repeat customers - Fast checkout for returning customers
- Orders API - Complete API reference
- Webhooks - Get notified when order status changes
You now know how to decouple order creation from payment execution. This pattern gives you the flexibility to build pre-orders, approval workflows, and any scenario where charging happens after order creation.