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)
A phone number and a "Pay" button. Submitting sends a real STK prompt — the customer never leaves the page.
Paybill (manual M-Pesa)
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
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
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:
If that phone number doesn't match an existing wallet, Create a wallet collects the minimum KYC needed to open one:
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:
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:

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

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:
From an authenticated wallet, Manage Wallet shows balance and recent ledger activity, and Top Up starts an STK-funded top-up:


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:

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

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


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.


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/transactionIdand, 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.