Accept a payment
Create a checkout session on your server, send the customer to the hosted page, and confirm the payment when they come back.
A checkout session is one attempt to collect a payment. You create it on your server with what the customer is buying, Lollipay hosts the payment page, and the session records the result.
1. Create the session
Send the line items from your server. Each item has a name and a unit amount in minor units, plus an optional
quantity (1 to 100) and a square HTTPS image.
curl https://api.lollipay.cc/v1/stores/$LOLLIPAY_STORE_ID/checkouts \
-H "Authorization: Bearer $LOLLIPAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Upgrade to Pro",
"items": [
{ "name": "Pro plan (yearly)", "amount": 4900 },
{ "name": "Extra seat", "amount": 1500, "quantity": 2 }
],
"amount": 7900,
"currency": "usd",
"customer": { "email": "buyer@example.com", "name": "Emma Wilson" },
"metadata": { "order_id": "A-1042" },
"return_url": "https://example.com/thanks",
"cancel_url": "https://example.com/pricing"
}'| Field | Notes |
|---|---|
items | 1 to 10 items. Together they must come to between 99 and 100,000,000 minor units ($0.99 at least). |
amount | Optional. The total you expect. If it doesn't match the items, the request fails with amount_mismatch, which catches pricing bugs before a customer sees them. |
currency | Three-letter ISO code. Defaults to your store's currency. |
customer | Prefills the email on the payment page; the receipt goes there. |
metadata | Up to 20 string values (keys up to 40 characters, values up to 500). Use it to link the session to your order. |
return_url | HTTPS page the customer goes to after paying. checkout_id is added to its query string. |
cancel_url | HTTPS page behind the payment page's back link. |
Store the session id with your order. The session stays open for 1 hour.
2. Send the customer to the payment page
Redirect the customer to checkout_url from the response, the session's own page at
https://checkout.lollipay.cc/invoice/cs_…. On a server-rendered site that's a 303 redirect from the form
handler; in a single-page app, set window.location to it.
export async function POST() {
const res = await fetch(`https://api.lollipay.cc/v1/stores/${process.env.LOLLIPAY_STORE_ID}/checkouts`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LOLLIPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [{ name: "Pro plan", amount: 4900 }],
return_url: "https://example.com/thanks",
}),
});
const { ok, data, error } = await res.json();
if (!ok) return Response.json({ error: error.message }, { status: 502 });
return Response.redirect(data.checkout_url, 303);
}3. Confirm the payment
When the customer lands on return_url?checkout_id=cs_…, retrieve the session on your server and check it before
you fulfil:
const res = await fetch(`https://api.lollipay.cc/v1/stores/${storeId}/checkouts/${checkoutId}`, {
headers: { Authorization: `Bearer ${process.env.LOLLIPAY_SECRET_KEY}` },
});
const { data: session } = await res.json();
if (session.status === "complete" && session.metadata.order_id === order.id) {
// Mark the order paid. session.payment_id identifies the payment in the dashboard.
}Don't trust the URL alone
Anyone can type ?checkout_id=… into a browser. Always retrieve the session with your secret key and check its
status, and that it belongs to the order you expect, before giving anything away.
Session statuses
| Status | Meaning |
|---|---|
open | Waiting for payment. Declined attempts keep the session open so the customer can retry. A crypto payment that's on its way keeps the session open, even past expires_at, until it has enough confirmations. So does a skins trade, for the 7 days Steam holds it. |
complete | Paid. payment_id and completed_at are set. |
expired | Not paid within 1 hour. Create a new session if the customer comes back. |
Crypto payments complete once the network confirms them: under a minute for Solana and Ethereum, 10 to 30 minutes for
Bitcoin and Litecoin. The customer
may close the page before that, so they don't always come back to return_url: a session that's still open when
they leave can turn complete later. Retrieve it again before you decide an order wasn't paid.
Skins payments (game items, through Steam) complete about a week after the customer accepts the trade, when Steam's
Trade Protection hold ends. The session stays open until then.