Charge repeat customers

Repeat customers expect fast, frictionless checkout. Instead of asking them to re-enter their name, email, and payment details every time, use the customer and payment method IDs from their first purchase to create instant checkout flows. This guide shows you how to charge returning customers with saved payment methods and how to handle cases where they want to use a different payment method.


How it works

After a customer's first successful payment, Commerce automatically creates a customer record and attaches their payment method to it. You'll find customer.id and payment_method.id in the order response. Store these IDs in your database linked to your user account. When the customer returns, reference them with customer_id and optionally payment_method_id instead of passing full details. Commerce handles OTP delivery and payment confirmation just like the first time, but checkout is faster because you're not collecting information again.


Choose your integration approach

Select how you want to charge repeat customers:

The simplest repeat customer flow uses both customer_id and payment_method_id for instant, one-click checkout. The customer doesn't need to re-enter any information—just confirm the OTP and the payment completes.

Charge with saved method

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_repeat_001",
    "execute_payment": true,
    "customer_id": "cus_abc123",
    "payment_method_id": "pm_saved_method",
    "line_items": [
      {
        "type": "product",
        "product": {
          "type": "digital",
          "name": "Premium Subscription",
          "quantity": 1,
          "price": { "currency": "ghs", "value": 5000 }
        }
      }
    ]
  }'

Response

{
  "order": {
    "id": "ord_repeat_xyz",
    "status": "requires_payment",
    "customer": {
      "id": "cus_abc123",
      "name": "Gloria Kesewaa",
      "email_address": "gloria@example.com"
    },
    "payment": {
      "id": "pay_new_attempt",
      "status": "requires_action",
      "payment_method": {
        "id": "pm_saved_method",
        "type": "mobile_money",
        "issuer": "mtn",
        "number": "0544998605"
      },
      "next_action": {
        "type": "confirm_payment",
        "confirm_payment": {
          "expires_at": "2025-01-13T10:08:00Z",
          "request": {
            "recipient": "0544998605",
            "sent_via": "sms"
          }
        }
      }
    }
  }
}

What's different:

  • No customer_data or payment_method_data needed—everything is already on file
  • Commerce sends OTP to the phone number associated with the saved payment method
  • Customer confirms with OTP just like the first purchase

The confirmation step is identical to first-time payments. Collect the OTP from the customer and call /orders/confirm_payment with the order ID and token.


Common patterns

Subscription renewals

For recurring subscriptions, store the customer and payment method IDs when they subscribe, then charge them automatically:

async function renewSubscription(subscriptionId: string) {
  const subscription = await db.subscriptions.findOne({ id: subscriptionId })
  
  const order = await client.orders.create({
    idempotency_key: `sub_${subscriptionId}_${new Date().toISOString()}`,
    execute_payment: true,
    customer_id: subscription.commerce_customer_id,
    payment_method_id: subscription.commerce_payment_method_id,
    line_items: [
      {
        type: 'product',
        product: {
          type: 'digital',
          name: `${subscription.plan_name} - Monthly`,
          quantity: 1,
          price: subscription.price,
        },
      },
    ],
  })

  // For subscriptions, Commerce may auto-confirm some payment methods
  // Check if requires_action or already paid
  if (order.payment.status === 'requires_action') {
    // Send OTP notification to customer
    await notifyCustomer({
      email: subscription.customer_email,
      message: 'Please confirm your subscription renewal with the OTP sent to your phone',
    })
  } else if (order.payment.status === 'paid') {
    // Payment completed automatically
    await updateSubscription(subscriptionId, { last_payment_date: new Date() })
  }
}

Testing

Use the same test numbers as standard payments. The key difference is you'll reference saved customer and payment method IDs:

# First-time purchase (creates customer and payment method)
curl https://api-test.zebo.dev/orders/new \
  -H "Authorization: Bearer TEST_KEY" \
  -d '{
    "customer_data": {
      "name": "Test User",
      "phone_number": "+233242000003"
    },
    "payment_method_data": {
      "type": "mobile_money",
      "mobile_money": { "issuer": "mtn", "number": "0242000003" }
    },
    "line_items": [...]
  }'

# Response includes customer.id and payment_method.id - save these

# Repeat purchase with saved IDs
curl https://api-test.zebo.dev/orders/new \
  -H "Authorization: Bearer TEST_KEY" \
  -d '{
    "customer_id": "cus_test_abc123",
    "payment_method_id": "pm_test_xyz789",
    "execute_payment": true,
    "line_items": [...]
  }'

# Confirm with test OTP (always 000000)
curl https://api-test.zebo.dev/orders/confirm_payment \
  -d '{"order_id": "test_order_id", "token": "000000"}'

Key tips

Store both IDs: Always save customer.id and payment_method.id from the response after the first payment. You need the customer ID to associate future orders and the payment method ID for fast checkout.

Customer profile vs payment method: A customer can have multiple payment methods. When you use customer_id + payment_method_data, you're adding a new payment method to an existing customer. When you use customer_id + payment_method_id, you're charging an existing payment method.

Payment method verification: All payment methods require OTP confirmation, even saved ones. This protects both you and the customer—it proves the customer has access to the phone at the time of payment.

Handle expired payment methods: Phone numbers change, accounts get closed. If a payment fails with an expired or invalid payment method error, prompt the customer to add a new payment method using the customer_id + payment_method_data pattern.


Next steps

You now know how to build fast, one-click checkout for repeat customers while maintaining security through OTP verification.

Was this page helpful?