Checkout
Convert a cart into a paid order. The body is **flat**: fields like ` cart_id`, `customer`, `order_type`, and `payment_method` sit at the top level (not nested under any envelope). Production uses ` #[serde(flatten)]`; the SDK matches that shape exactly.
Process a checkout
const r = await client.checkout.process({
cart_id: cart.id,
location_id: 'loc_main',
order_type: 'pickup', // 'delivery' | 'pickup' | 'dine-in' | 'walk-in'
payment_method: 'mobile_money', // 'mobile_money' | 'card'
customer: {
name: 'Jane Doe',
email: 'jane@example.com',
phone: '+233244000000',
save_details: true,
},
address_info: {
pickup_time: '2026-05-08T15:00:00Z',
},
mobile_money_details: {
phone_number: '+233244000000',
provider: 'mtn',
},
special_instructions: 'No straw, please.',
})
if (!r.ok) {
console.error(r.error.code, r.error.message)
return
}
const result = r.value
console.log(result.order_id, result.order_number)
console.log(result.payment_status) // 'pending' | 'success' | 'failed' | ...
console.log(result.requires_authorization)
console.log(result.next_action) // discriminated union, see belowbill_token: guest order lookups
On a successful guest checkout the response carries a bill_token. The SDK persists it client-side keyed by order_id, then attaches it to every subsequent orders.get / checkout.pollPaymentStatus call. You do not need to thread it through manually; it just works in the same browser session.
if (r.ok && r.value.bill_token) {
// Already cached on the client; this read is purely for your records.
// No need to pass it back in subsequent calls.
console.log(r.value.bill_token)
}next_action: branch on what to do next
switch (result.next_action?.type) {
case 'none':
// Payment captured. Show a confirmation screen.
break
case 'redirect':
window.location.href = result.next_action.authorization_url
break
case 'card_popup':
openProviderPopup({
provider: result.next_action.provider,
clientSecret: result.next_action.client_secret,
publicKey: result.next_action.public_key,
})
break
case 'authorization':
// Collect OTP / PIN / phone / birthday / address from the user, then submit
promptForAuthorization(result.next_action.authorization_type, result.next_action.display_text)
break
case 'poll':
pollUntilTerminal(result.order_id)
break
}submitAuthorization
When the customer completes an OTP / PIN / etc., POST it back through the SDK. The response is the same CheckoutResult shape, with an updated next_action.
const r = await client.checkout.submitAuthorization({
order_id: 'ord_xxx',
authorization_type: 'otp',
authorization_value: '123456',
})
if (!r.ok) {
console.error(r.error.code, r.error.message)
return
}
if (r.value.next_action?.type === 'poll') {
await pollUntilTerminal(r.value.order_id)
}pollPaymentStatus
async function pollUntilTerminal(orderId: string, intervalMs = 2000, maxAttempts = 30) {
for (let i = 0; i < maxAttempts; i++) {
const r = await client.checkout.pollPaymentStatus(orderId)
if (!r.ok) return r
const status = r.value.status
if (status === 'success' || status === 'failed') {
return r
}
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
}Attach a customer to a guest order
// After a guest checkout, attach customer info to the order (uses bill_token automatically)
const r = await client.checkout.updateOrderCustomer('ord_xxx', {
name: 'Jane Doe',
email: 'jane@example.com',
phone: '+233244000000',
save_details: false,
})Cross-currency checkout
Set pay_currency on the request and the SDK locks an FX quote for you behind the scenes; the fx field on the response shows the locked rate.
const r = await client.checkout.process({
cart_id: cart.id,
customer: { /* ... */ },
order_type: 'pickup',
payment_method: 'card',
address_info: {},
pay_currency: 'USD',
})
if (r.ok && r.value.fx) {
console.log(r.value.fx.base_amount, '→', r.value.fx.pay_amount)
console.log(r.value.fx.rate, r.value.fx.quote_id)
}CheckoutFormData (request body)
| Field | Type | Notes |
|---|---|---|
cart_id | string | Required. |
customer | CheckoutCustomerInfo | Required. name, email, phone, save_details. |
order_type | 'delivery' | 'pickup' | 'dine-in' | 'walk-in' | Required. |
payment_method | 'mobile_money' | 'card' | Required. |
address_info | CheckoutAddressInfo | Required (can be empty for pickup). |
location_id | string | Multi-location businesses. |
mobile_money_details | MobileMoneyDetails | Required when payment_method = 'mobile_money'. |
special_instructions | string | Free-form note for the order. |
idempotency_key | string | Auto-generated if omitted. |
pay_currency | CurrencyCode | Triggers FX quoting if different from cart currency. |
fx_quote_id | string | Pre-locked quote (otherwise the SDK locks one). |
metadata | Record<string, unknown> | Round-tripped to webhooks. |
Method reference
| Method | Returns |
|---|---|
process(data) | Result<CheckoutResult> |
submitAuthorization(input) | Result<CheckoutResult> |
pollPaymentStatus(orderId) | Result<PaymentStatusResponse> |
updateOrderCustomer(orderId, customer) | Result<Order> |
verifyPayment(orderId) | Result<Order> |
Related
-
Cart The
cart_idinput comes from here -
Orders Read the order created by checkout.process
-
FX Rates and locked quotes for cross-currency pay
-
Error handling PAYMENT_FAILED, AUTH_INCOMPLETE, recovery flows
Cart
Session-bound cart. Add items with a flat payload, apply coupons, and read totals as branded ` Money` strings. The SDK returns `Result<T>` on every method and never throws.
Authentication
OTP-based sign-in over phone or email. No passwords. The SDK manages the session token end-to-end; a successful `verifyOtp` call wires the token into every subsequent request.