# Checkout

> A hosted checkout page that quotes the tax from where the customer is, takes the payment, and records the sale only when the processor confirms it, with the invoice, the ledger and the thresholds in one step.

Checkout sells your **one-time prices** and **credit packs** on a page Ownrecord hosts. You create a **checkout session** with the API, send the customer to its URL, and get the result on your success page and in webhooks. Sessions in `subscription` mode start [subscriptions](https://ownrecord.io/docs/concepts/subscriptions.md#subscribing-through-checkout), with trials and promotion codes, sessions in `topup` mode add credits to an existing customer's [wallet](https://ownrecord.io/docs/concepts/wallets.md#buying-credits-top-ups-and-credit-packs), and sessions in `setup` mode save a [payment method](https://ownrecord.io/docs/concepts/payment-methods.md).

## Creating a session

```http
POST /api/v1/checkout/sessions
Idempotency-Key: 6f1c…

{
  "lineItems": [{ "lookupKey": "credits_100", "quantity": 1 }],
  "customerEmail": "anna@example.com",
  "successUrl": "https://shop.example/thanks",
  "cancelUrl": "https://shop.example/cart",
  "metadata": { "order": "ord_1042" }
}
```

- **Line items:** 1 to 20 prices of one currency, by ID or [lookup key](https://ownrecord.io/docs/concepts/catalog.md), with a quantity from 1 to 1,000. For a price with a custom amount (for example "any amount from €5"), give `customAmount` in integer micros; it's sold one at a time. Archived prices are refused, and so are recurring prices outside `subscription` mode.
- **Customer:** an existing customer, or an email to pre-fill. Buyers become [customers](https://ownrecord.io/docs/concepts/customers.md) when their details are quoted.
- **Top-ups:** `mode: "topup"` sells exactly one credit pack, a fixed pack or a custom amount, to the `customer` you name (required). The page asks for that customer's email, and a customer who has paid you before is taxed as an existing customer.
- **Business details:** `collectBusinessDetails` is `auto` (the page asks "Buying for a business?"), `required` or `never`.
- **Expiry:** a session lives 24 hours by default; `expiresAt` can shorten it to as little as 30 minutes. `checkout.sessions.expire` ends it at once.

The answer is the session with its `url`: `https://ownrecord.io/c/{brand}/{session}?key=…`. The `key` proves the link came from you, because session IDs alone are guessable. Give the URL only to the customer. Once a session uses a price, that price can't change any more.

## What the customer sees

The page is mobile-first, shows your brand, and in test mode a banner that says no real money moves. The footer is the merchant disclosure: **Sold by** your legal entity, its address and its VAT ID.

1. **Details.** Email and country, pre-filled from the customer's IP address, and for a business the company name, VAT ID and address. The VAT ID is checked with its register (VIES, HMRC or the Swiss UID register).
2. **Quote.** The [tax engine](https://ownrecord.io/docs/concepts/tax-engine.md) works out the tax from the location evidence: the billing country and the IP country, and later the card's country. The page shows each line, the tax as "VAT 19% (Germany)" (or "Includes VAT …" for prices that include tax, or "Reverse charge" for a business with a valid VAT ID) and the total.
3. **Payment.** Ownrecord prepares the payment at your entity's [processor](https://ownrecord.io/docs/concepts/processors.md) for exactly the quoted total. With Stripe, the customer pays in the Payment Element; in development and tests, the fake processor offers test cards. When the card is entered, its country joins the evidence and the sale is quoted again. If the total changes, the customer sees the new total and confirms it before anything is charged. With Mollie, the customer accepts the terms here and pays on Mollie's payment page, with a method your Mollie profile offers (cards, iDEAL, Bancontact, …), then comes back to the success page; the sale is invoiced at the quote they paid for.
4. **Legal consents.** The customer accepts your terms. Consumers in the EU and the UK buying digital services that start at once also agree to immediate performance and the loss of their withdrawal right; Ownrecord stores that consent with its time, text version and the customer's IP country.
5. **Confirm.** 3-D Secure runs when the bank asks for it.
6. **Success.** The page waits until the payment is recorded, then sends the customer to your success URL with `?cs_id={session}`.

A quote is valid for 30 minutes and is computed again when it expires or the date changes.

## When a sale can't go ahead

If Ownrecord can't tax the sale with confidence, it doesn't take the payment: you sell only where your tax position is clear. The session becomes `blocked`, you get `checkout.session.blocked`, and the page explains why:

- **Not registered yet** (for example the UK, which requires registration before the first consumer sale): "Genno can't sell to consumers in the United Kingdom yet". The customer can **join the waitlist**: "Notify me when Genno can sell in the United Kingdom". The entry is confirmed by a link in an email (double opt-in, valid for 7 days). A business with a valid VAT ID may still buy, reverse charged, where the rules allow it.
- **Unclear location**: the evidence doesn't confirm one country. The customer can check their country and try again.

Nothing about a blocked customer is stored except the session and, if they ask, their waitlist entry. Ownrecord counts each paused checkout (per country, regime and reason, once per session) without anything about the customer. `GET /api/v1/waitlist_entries` lists your waitlist; the entity's **Demand** page ([registrations](https://ownrecord.io/docs/concepts/registrations.md#demand-where-it-pays-to-register)) shows where it pays to register.

### When you can sell there

As soon as you can sell to consumers in a waitlisted customer's country, Ownrecord emails them **once**: "Genno can now sell in the United Kingdom". That happens when a registration change lets you sell there (you arm the registration, or mark it registered) and, daily, when the tax knowledge changes. Before emailing, Ownrecord quotes what the customer wanted to buy, for a consumer in their country, the way a checkout would; only a sale that would go through is announced.

- **One email per opening.** An entry is marked `notifiedAt` and never emailed again for that opening, even if the registration changes again later. A customer who joins the waitlist again after that starts a new double opt-in and hears about the next opening.
- **A fresh checkout.** The email's **Continue to checkout** link makes a new checkout session at the click, with the lines, URLs and settings of the session the customer joined from, so it works long after a session's 24 hours. The session's metadata has `waitlist_entry`.
- **Leaving.** Both waitlist emails have a **Leave the waitlist** link (`unsubscribedAt`): a customer who leaves is never emailed.
- **Rate.** At most 50 emails a minute per legal entity and mode; the rest follow a minute later.

The hosted pages for these links call public operations you can call too: `checkout.hosted.resume_waitlist` and `checkout.hosted.leave_waitlist`, each with the token from the email's link.

## Fulfilment: only on the processor's word

A purchase is recorded only when the processor's webhook says the payment succeeded, never because the browser reached the success page. Then, in one database transaction, Ownrecord:

- records the **payment**;
- posts the sale to the [ledger](https://ownrecord.io/docs/concepts/ledger.md) (revenue per product and tax per jurisdiction and regime), or funds the customer's wallet for a credit pack or a top-up;
- applies the tax point: a registration change the sale triggers (for example the start of a grace period when it crosses a threshold) and the **threshold counters** of your legal entity, shared by all its brands;
- locks the customer's tax country at their first payment (a later top-up taxed in another country moves it);
- issues the [invoice or receipt](https://ownrecord.io/docs/concepts/invoices.md) with the next number of your brand's series;
- marks the session `complete` and queues the events.

The invoice shows exactly the quote the customer paid, even if that quote has expired or your registration changed since. If the amount paid isn't that quote's total (it never should be), nothing is recorded and a `RECON_UNMATCHED` alert opens for review.

## Statuses and events

| Status       | Meaning                                                              |
| ------------ | -------------------------------------------------------------------- |
| `open`       | Waiting for the customer                                             |
| `blocked`    | The sale is paused for the customer's location; the waitlist is open |
| `processing` | The customer confirmed; waiting for the processor                    |
| `complete`   | Paid and fulfilled: `paymentId` and `invoiceId` are set              |
| `expired`    | The session's time ran out, or you expired it                        |

[Webhooks](https://ownrecord.io/docs/concepts/webhooks.md) deliver `checkout.session.completed`, `checkout.session.blocked`, `checkout.session.expired`, `payment.succeeded`, `payment.failed`, `invoice.issued` and `customer.created` or `customer.updated`.

## Building your own page

The hosted page uses public operations you can call too: `checkout.hosted.get`, `submit_details`, `start_payment`, `attach_payment_method`, `confirm` and the waitlist steps. They take the session key from the URL, or an API key with `checkout:write`, so you can build your own checkout on the same tax quotes and fulfilment.
