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 $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "request_meta": {
    "idempotency_key": "order_repeat_001"
  },
  "execute_payment": true,
  "customer_id": "cu_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": "or_repeat_xyz",
    "status": "requires_payment",
    "customer": {
      "id": "cu_abc123",
      "name": "Gloria Kesewaa",
      "email_address": "[email protected]"
    },
    "payment": {
      "id": "py_new_attempt",
      "status": "requires_action",
      "payment_method": {
        "id": "pm_saved_method",
        "type": "mobile_money",
        "network": "mtn",
        "account_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. Submit the returned order ID, payment ID, confirmation-request ID, and the customer's token as shown in Confirm a payment.


Common patterns

The example below uses a native HTTPS helper and unwraps the API's { order } response envelope:

async function createOrder(payload: unknown) {
  const response = await fetch('https://api.zebo.dev/orders/new', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.COMMERCE_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  })
  if (!response.ok) throw new Error(await response.text())
  const { order } = await response.json()
  return order
}

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 createOrder({
    request_meta: {
      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 an application configured for the test environment and send the opaque API key from the dashboard to the standard API host. Create the first test order to obtain customer and payment-method IDs, then use those IDs on the repeat order. If the response requires confirmation, follow Confirm a payment with order_id, payment_id, confirmation_id, and the test token supplied for that environment. Do not hardcode a universal token value.


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?