Skip to main content

Inside the checkout page

Hosted checkout covers the API side of a payment link — create an invoice, get back a checkoutUrl. This page shows what's actually on that link: every screen a customer can land on, from a real sandbox session. Useful if you want to know what to expect before sending a link to a customer, or which states to explain in your own support docs.

The card is self-contained and centered on the page — your name, logo initials, and invoice description appear at the top; everything below is driven by the invoice's amount and methods.

Payment methods​

The tab bar shows exactly the methods you passed in methods when creating the invoice (or every method your company has configured, if you omitted it). A customer switches between tabs freely until they commit to one.

M-Pesa (STK push)​

M-Pesa STK push tab: phone number field and Pay button

A phone number and a "Pay" button. Submitting sends a real STK prompt — the customer never leaves the page.

Paybill (manual M-Pesa)​

Paybill tab: paybill number, account number, amount, with copy buttons

For customers who'd rather pay from their own M-Pesa menu. The paybill number comes from your configured gateway; the account number is always the invoice number, pre-filled so the customer doesn't have to know your reference scheme. "I've Paid" moves it to a pending, polling state — see While a payment is in flight.

Card​

Card tab: card number, expiry, CVC, and name fields

A direct card form — number, expiry, CVC, name. Submission is synchronous: card data passes straight through to the gateway and is never stored or logged (see security notes below).

Bank transfer​

Bank tab: bank name, account name, account number, branch, and reference

Same shape as paybill — instructions plus "I've Paid" — but for a direct bank transfer (Pesalink). The reference is again the invoice number.

Paying with a wallet​

Wallet is the most involved method, because it's the only one that identifies the customer. Nothing here is something your integration drives — it's entirely self-serve on this page, authenticated by SMS OTP (see Customers & wallets for why that's not an API you call).

No session yet — a customer opening a wallet tab for the first time on this device just sees a phone field:

Wallet tab with a phone number field and Continue button

If that phone number doesn't match an existing wallet, Create a wallet collects the minimum KYC needed to open one:

Create Wallet form: first name, last name, middle name, national ID, phone number

Either path — existing or new — lands on the same OTP screen. The code is texted to the masked number shown; it's a real one-time code, not a placeholder:

OTP screen: six-digit code entry, verify button

Once verified, what the customer sees depends on their balance relative to the invoice amount:

Insufficient balance — a top-up prompt instead of a pay button:

Wallet balance too low: shows balance and a Top Up Wallet button

Sufficient balance — one tap to pay. Note the tab bar only shows the methods this invoice allows (here, just Wallet and M-Pesa):

Wallet balance sufficient: name, balance, and a Use Wallet button

A returning customer on the same device skips straight past the phone field — Stripe Link-style, a long-lived cookie remembers who they are (never their balance or a live session), and a fresh OTP is still required to actually authenticate:

Continue as [name] one-tap card, with a link to use a different account

From an authenticated wallet, Manage Wallet shows balance and recent ledger activity, and Top Up starts an STK-funded top-up:

Manage Wallet screen: balance card and a list of recent credits and debits
Top Up Wallet form: amount and M-Pesa phone number fields

While a payment is in flight​

Two waiting states, depending on the method:

STK push / wallet top-up — waiting on the customer to enter their PIN:

Check your phone: waiting for STK PIN entry, with a cancel link

Paybill / bank — waiting on a manual transfer to be reconciled:

Confirming your payment: polling state after I've Paid

Both poll quietly in the background; the customer doesn't have to refresh anything.

Outcomes​

Payment Successful screen with a Done button
Card tab showing an inline decline error above the Pay button

A decline (shown here: a real sandbox card rejection) surfaces inline above the button, on the same tab — the customer can just fix the details and retry. Nothing about the session resets.

This payment link has expired message
This payment link is no longer active message

Both are terminal and unrecoverable from this page — per Expiry, you create a fresh invoice if the customer still needs to pay.

What never touches your server​

  • Card numbers, CVCs, OTP codes, and wallet sessions exist only on this hosted origin — your backend only ever sees the invoiceNo / transactionId and, eventually, a webhook.
  • The page reads and writes directly against the same data your invoice lives in, so what's shown here — amount, methods, status — is always exactly what you set when you created the invoice.
  • Want this experience inside your own page instead of a redirect? See Flow 2: Payment links — it's the same page, rendered in an iframe via the browser SDK, with lifecycle events posted back to you instead of a redirect.