Guide
Hosted checkout
Send the payer to a Lint page that takes the payment, renews expired bank details by itself and confirms on screen, then hear about it by webhook.
A checkout session is one payment you want to collect. Your server creates it and gets back a url. You send the payer there, Lint's page takes the payment by bank transfer, and you get a signed webhook when it lands. The money goes into your Lint wallet, less Lint's fee.
Use this when you'd rather not build the payment screen yourself. If you want to show bank details in your own UI, use one-time virtual accounts directly.
The flow
- Your server creates a session for the order's amount.
- You redirect the payer to the session's
url. - The page shows your business name, the amount and your description, and gives the payer a bank account for exactly that amount. Bank accounts last up to 24 hours. If one lapses before the payer pays, the page issues a fresh one.
- The payer transfers from any bank app. The page confirms within seconds and sends them to your
success_url. - Lint posts
checkout.session.completedto your webhook endpoints. You confirm and fulfil the order.
Create a session
curl -X POST https://staging.lint.finance/api/partner/v1/checkout/sessions/create \
-H "Authorization: Bearer $LINT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: create-order-8812" \
-d '{
"amount": 1250000,
"currency": "NGN",
"reference": "order_8812",
"description": "2 x Golden Morn 12x500g",
"customer": { "name": "Ada Obi", "email": "ada@example.com" },
"success_url": "https://shop.example.com/orders/8812/thanks",
"cancel_url": "https://shop.example.com/cart",
"metadata": { "cart_id": "c_19" }
}'| Field | Notes |
|---|---|
amount | Required. In kobo: 1250000₦12,500.00. At least 100. |
currency | Required. NGN. |
reference | Your order ID. Unique across your sessions; comes back on the session and in every webhook. Send one. |
description | Shown to the payer under the amount. |
customer | name, email, phone, for your records. Not shown to the payer. |
success_url | Where the payer goes after paying, with session_id added to the query string. |
cancel_url | Where the payer goes if they back out, or come back after the session expired. |
expires_in | Seconds until the session expires unpaid: 900 (15 minutes) to 604800 (7 days). Defaults to 24 hours. |
metadata | Up to 20 string values of your own, returned as sent. Never shown to the payer. |
{
"data": {
"id": "9d3c1f0a-5b2e-4c7a-9f10-2b3c4d5e6f70",
"status": "open",
"amount": 1250000,
"amount_paid": 0,
"currency": "NGN",
"reference": "order_8812",
"url": "https://lint.finance/checkout/Xk3n9QpL2vT8rW1y…",
"expires_at": "2026-11-04T10:00:00+00:00",
"payment": null,
"…": "…"
},
"message": "Checkout session created"
}Store id against your order and redirect the payer to url. Send an Idempotency-Key so that retrying a timed-out request returns the same session instead of creating another.
Or open it on your page
Rather than redirecting, you can open the same checkout in an overlay on your own page. Load the script once, then call LintCheckout.open with the session's url. Your server still creates the session; the browser never needs a key.
<script src="https://lint.finance/v1/checkout.js"></script>
<script>
payButton.addEventListener('click', async () => {
// Your server creates the session and returns its url
const session = await fetch('/orders/8812/lint-session', { method: 'POST' }).then((r) => r.json())
LintCheckout.open({
url: session.url,
onSuccess: () => showConfirming(), // the page saw the payment: confirm before fulfilling
onExpired: () => offerToStartAgain(),
onClose: (e) => {
// e.reason is "closed" or "cancelled"; e.paid says whether it was paid first
if (!e.paid) showBasketAgain()
}
})
})
</script>| Callback | When |
|---|---|
onReady | The checkout has loaded in the overlay. |
onSuccess | The payer has paid. Fires once. |
onExpired | The session reached expires_at unpaid. |
onClose | The overlay closed: the payer pressed Done, the close button, Cancel, Escape or the backdrop. { reason, paid }. |
The overlay is full screen on phones. In the overlay, success_url and cancel_url aren't used: what happens next is up to your page. The same script works for sandbox and live; it opens whichever url the API gave you.
Treat onSuccess like the redirect: a prompt, not proof. Fulfil on the checkout.session.completed webhook, or after GET /checkout/sessions/{id} says paid.
Hear about the payment
When the payer has paid in full, Lint sends checkout.session.completed to every endpoint you've subscribed to it. data is the session, now paid, with a payment block showing the amount paid, Lint's fee and what reached your wallet.
- Verify the signature and reply
200straight away. - Look up your order by
reference, or by the sessionid. - Mark the order paid, once. The same event can be delivered again.
If the payer never pays, you get checkout.session.expired at expires_at. Release whatever you were holding for the order.
An expired session can still be paid. A transfer the payer started just before expires_at can land just after it, and then checkout.session.completed follows checkout.session.expired. The money is in your wallet, so fulfil the order or refund the payer. Don't ignore it.
Don't trust the redirect
The payer arriving at your success_url means the page saw the payment. But anyone can type that URL. Before you fulfil, confirm with the webhook or the API:
curl https://staging.lint.finance/api/partner/v1/checkout/sessions/9d3c1f0a-5b2e-4c7a-9f10-2b3c4d5e6f70 \
-H "Authorization: Bearer $LINT_ACCESS_TOKEN" \
-H "Accept: application/json"Fulfil only when status is paid.
Test it in the sandbox
Create a session and open its url: the page shows bank details straight away. Then pay the session without a real transfer:
The page flips to Payment received and your endpoints get checkout.session.completed. To test expiry, create a session with expires_in: 900 and leave it for 15 minutes.