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

POST
/orders/new
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

{
  "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 later
  • status: "preparing" - Order exists but no payment initiated yet
  • payment object 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

POST
/orders/pay
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

{
  "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 payment object with payment intent details
  • payment.status: "requires_action" - OTP sent to customer
  • Order status still requires_payment until 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

POST
/orders/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

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.

Was this page helpful?