Skip to content
Coritan Docs

Create hosted checkout pages with the Merchant API

Create a payment page for an amount, serve it on your own domain in your own style, and learn when it is paid from the API or a webhook.

View as Markdown

A hosted checkout page is a page you create for one amount. It has no products and no cart. We serve it on a domain of your own, and your customer sees the amount, a billing form and your own words and look. When the customer sends the form, they go on to your MAEF parent site, the payment site, and pay there. Nobody enters a card number on the page, and nothing on it names Coritan.

You style the whole page yourself with a set of style keys and your own CSS. Hosted checkout pages are optional, and our staff turn them on for each store.

The page is not a store checkout. It makes no order, no invoice and no entry on your balance. We tell you when the page is paid, as Confirm the payment on your server describes, and you refund at your payment site.

The routes are under https://api.coritan.com/api/v1/orgs/{org_slug}/commerce, and the API reference lists them under Commerce. The dashboard does the same in Checkout pages, as Create and style hosted checkout pages describes.

  • Our staff turn hosted checkout pages on for a store: in the admin console they tick "Allow hosted checkout pages" on the store's Payments card. Ask support. Until then, every write answers 409 with hosted_checkout_not_enabled, and the two lists answer with hosted_checkout_enabled: false.
  • The store needs to be in test or live mode. A live page needs a live store.
  • Add at least one payment page for the mode and currency you charge in, and turn it on. A store that has only hosted checkout pages can add payment pages without turning on payment pages at checkout.
  • Have a hostname you can add DNS records to, such as pay.example.com. It serves checkout pages only.
  • Creating and cancelling pages and managing domains takes an organization API key with the commerce.checkout:write scope, or a member's token with the Admin role or above. Reading them, and the preview, takes commerce.checkout:read, or the Billing role or above.

Send the key in the X-API-Key header, as Authenticate describes. The examples use $ORG_API_KEY and an organization called acme.

A checkout domain is a hostname that serves checkout pages and nothing else. Any other address on it answers a plain "Page not found". It can sit on a different domain from the rest of your site.

Important

Never use the host of your storefront. A checkout host answers /thank-you itself, and your storefront's /thank-you belongs to your store's payment pages.

Add the hostname:

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-domains" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "pay.example.com"}'

The answer is 201 with the checkout_domain. Adding a name you already added answers with it as it is.

JSON
{
  "checkout_domain": {
    "hostname": "pay.example.com",
    "verified": false,
    "verified_at": null,
    "records": [
      {"type": "TXT", "name": "_coritan-checkout.pay.example.com", "value": "3f9c1d7a5b2e4c80a6d1f0e9b8c7a654"},
      {"type": "CNAME", "name": "pay.example.com", "value": "edge.example.net"}
    ],
    "certificate": "none",
    "created_at": "2026-10-14T09:30:00Z"
  }
}

Publish the records at the DNS provider of the domain:

Record What it does
TXT at _coritan-checkout.<hostname> Proves you own the name. Its value is a token we made for this hostname.
CNAME at <hostname> Points the name at us. It is listed only when staff have set a target for it (the platform setting commerce.checkout_cname_target). Without one, records holds the TXT record alone, and the dashboard shows no CNAME.

Then ask us to check the TXT record:

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-domains/pay.example.com/verify" \
  -H "X-API-Key: $ORG_API_KEY"

When the record is there, the answer has verified: true. We then order a certificate for the name, and certificate is one of:

certificate What it means
none The domain is not verified yet.
pending The domain is verified and its certificate is being issued. The name must already point at us for that to finish.
active The certificate is installed and customers can open pages on the name.

GET /commerce/checkout-domains lists the domains, oldest first, with hosted_checkout_enabled, cname_target (a name or null) and limit. Read it again until certificate is active before you send a customer to a page.

Answer Why
422 with invalid_hostname The name is not a hostname. It needs at least two parts, no port and no IP address. A scheme, path, query or fragment is cut off.
422 with host_not_allowed The name belongs to the platform, is under the domain App Deployment gives to deployments, or is your storefront's domain or address. reason says which: platform_host, app_domain or storefront.
409 with host_in_use Another organization or a site of ours already uses the name. A name that another organization added and never verified frees up after 72 hours.
409 with limit_reached You have 10 domains. Remove one first.
409 with verification_failed The TXT record is missing or wrong. The answer lists what we found. DNS changes can take a while to spread, so check again in a few minutes.
429 You asked to verify one hostname more than 12 times in an hour, or changed domains more than 60 times in an hour.
Shell
curl -X DELETE "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-domains/pay.example.com" \
  -H "X-API-Key: $ORG_API_KEY"

The answer is {"deleted": true}. Pages already made on that hostname stop opening. While a page there is processing and its customer started to pay in the last 30 minutes, it answers 409 with domain_in_use. Remove the domain once that payment is done.

Tell the payment site where to send the customer

Section titled Tell the payment site where to send the customer

After the customer pays, the payment site sends them back to your checkout host with maef_complete, sig and maef_parent in the query. That return is how we learn the page is paid. We check its signature with the secret of your payment page, and nothing else makes a page paid.

We hand the payment site the return address of each page, https://<your checkout host>/c/<page id>/return, and your checkout host as the child site's domain. We also answer /thank-you on the checkout host. There we find the page from the signed order number in the return, or from the customer's email and the amount when the return has no order number.

Warning

We have not tested hosted checkout pages against a live payment site. A payment site that sends customers only to the address it holds for your storefront will send them there instead, where the store does not recognise a page's payment. Register your checkout host with the payment site the way you registered your storefront, and make a small test payment before you send a customer to a page.

If the return never arrives, the page stays processing. Troubleshooting says what to check.

A page needs an amount and a return_url, the address the customer came from. Everything else is optional.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-pages" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2500,
    "currency_code": "USD",
    "livemode": true,
    "reference": "INV-1042",
    "description": "Annual hosting plan",
    "return_url": "https://example.com/invoices/1042",
    "success_url": "https://example.com/thanks",
    "cancel_url": "https://example.com/invoices/1042?cancelled=1",
    "host": "pay.example.com",
    "expires_in_minutes": 4320,
    "style": {"heading": "Pay invoice INV-1042", "accent_color": "#0f766e"}
  }'
Field Takes
amount Required. What the customer pays, as a whole number of minor units such as cents, from 1 to 99,999,999.
currency_code An ISO 4217 code such as USD. It must be one of the store's currencies. The store's default currency when left out.
livemode true makes a live page, which needs a live store. false, the default, makes a test page.
reference Your own reference, such as an invoice number, up to 255 characters. GET /commerce/checkout-pages filters by it.
description The name of the payment line on your payment site's own page, up to 120 characters. It is Payment when left out. The customer never sees it on the checkout page.
parent_product_id The product number your payment site uses for the payment line, a whole number of 0 or more. 0 by default.
style Style keys for this page. They are laid over the store's default style, as Style the page describes.
return_url Required. Where the customer came from, such as the invoice page on your site. An https address of up to 2,000 characters, with no spaces and no sign-in details. Send the customer back says where we use it.
success_url An https address, with the same limits, that a customer who has paid goes to instead of return_url.
cancel_url An https address, with the same limits, that a customer who leaves the form without paying goes to instead of return_url.
host One of your verified checkout domains. Your oldest verified domain when left out.
expires_in_minutes How long the page takes payments, from 5 to 43,200. 1440, one day, when left out.

The answer is 201 with the page and its url, the address to send the customer to:

JSON
{
  "checkout_page": {
    "id": "chk_01j9k2m4n6p8q0r2s4t6v8w0x2",
    "livemode": true,
    "status": "open",
    "amount": 2500,
    "currency_code": "USD",
    "reference": "INV-1042",
    "description": "Annual hosting plan",
    "parent_product_id": 0,
    "host": "pay.example.com",
    "url": "https://pay.example.com/c/chk_01j9k2m4n6p8q0r2s4t6v8w0x2",
    "return_url": "https://example.com/invoices/1042",
    "success_url": "https://example.com/thanks",
    "cancel_url": "https://example.com/invoices/1042?cancelled=1",
    "email": null,
    "expires_at": "2026-10-17T09:35:00Z",
    "paid_at": null,
    "parent_order_id": null,
    "attempts": 0,
    "style": {"heading": "Pay invoice INV-1042", "accent_color": "#0f766e", "theme": null, "font": null, "custom_css": null},
    "created_at": "2026-10-14T09:35:00Z"
  }
}

The style is the merged style we saved on the page, with every key present. It is shortened here. success_url and cancel_url are null when you did not send them. email is the customer's address once they have sent the form, and parent_order_id is the payment site's own number for the payment once it is paid.

GET /commerce/checkout-pages/{checkout_page_id} answers one page. GET /commerce/checkout-pages lists them, newest first, and takes these query parameters:

Parameter Takes
status open, processing, paid, failed, canceled or expired. A page whose time has passed counts as expired. Any other word answers 422.
reference Only pages with exactly this reference.
livemode true for live pages, false for test pages. Both when left out.
limit How many pages, 1 to 200. 50 by default.
before_id Send the next_before_id of the last answer to read the next page.

The answer is {"checkout_pages": [...], "next_before_id": ..., "hosted_checkout_enabled": true}. next_before_id is null on the last page.

Answer Why
409 with hosted_checkout_not_enabled Staff have not turned hosted checkout pages on for the store. Ask support.
409 with checkout_domain_required You have no verified checkout domain.
409 with no_payment_page The store has no payment page that is turned on for that mode and currency.
403 with store_not_live A live page on a store that is not live, or a page on a store that is not open, such as a suspended one.
422 with field The input is wrong, and field names it: amount, currency_code, reference, description, parent_product_id, return_url, success_url, cancel_url, expires_in_minutes, style or a style key such as style.accent_color or style.custom_css. A host that is not one of your verified checkout domains names host. An empty return_url, or one that is not an https address, a return_url over 2,000 characters, or one with spaces or sign-in details, names return_url.
422 for a missing return_url The request has no return_url, or sends null for it. The answer is the usual validation error: detail lists an entry with loc set to ["body", "return_url"] and type set to missing. We save no page.
429 with rate_limited You created more than 600 pages in an hour. Retry-After says when to try again.

Every page needs a return_url, the address the customer came from. A page with no way back is never made. We send the customer back to your site in these situations:

Situation The customer goes to
The customer pays and returns from the payment site We redirect with 303 to success_url, or to return_url when you gave no success_url. The customer sees no receipt.
The customer opens a paid page again A receipt with a "Continue" link to the same address.
The customer leaves from the billing form The "Cancel and go back" link, always shown, goes to cancel_url, or to return_url when you gave no cancel_url.
Any other page that knows its page: confirming, failed, held for review, expired or cancelled A "Return to site" link to return_url, beside "Try again" or "Check again" where the page has them.
A page that does not exist, and error pages No link. Nothing says which page the customer meant.

We add checkout_page=<id>&status=<status> to the query of each address, and keep any query and fragment it already has. If the query already has a checkout_page or a status, ours replace them. We send the address as plain ASCII, so a host with other characters goes out as punycode and any other character is percent-encoded. status is the page's status at the moment we make the link: open, processing, paid, failed, canceled or expired. A customer can follow a link before we have applied the payment site's return, so the status can be behind.

Confirm the payment through the API or the event, never from that query, as Confirm the payment on your server describes.

Every key is optional. The page has a built-in look, and each key you set changes one part of it.

The default style and the style of one page

Section titled The default style and the style of one page

The store has a default style, which every page uses. Set it with PATCH /commerce/store, and send null to clear it:

Shell
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/commerce/store" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"settings": {"checkout_style": {"accent_color": "#0f766e", "font": "inter", "radius": "round"}}}'

This needs the commerce.store:write scope. GET /commerce/store shows the default in settings.checkout_style.

The style you send with a page is laid over the default. A key you send wins, a key you leave out keeps the default, and a key set to null goes back to its built-in value. We save the merged result on the page, so changing the default later does not move pages you made already.

A key that is not a style key, or has a bad value, answers 422 with field set to style.<key>, or to settings.checkout_style when you set the default.

Key Takes
theme light or dark. Light by default.
accent_color A colour as #rrggbb. The button and the links.
background_color A colour as #rrggbb. Behind the card.
surface_color A colour as #rrggbb. The card. A plain layout has no card, so it uses the background colour.
text_color A colour as #rrggbb. Headings and body text.
radius sharp, soft or round. The corners of the card, the button and the fields. soft by default.
font system, inter, plex, serif or mono. system by default.
layout card or plain. card by default.
logo_url An https address, up to 2,048 characters, with no spaces or quotes.
heading Up to 120 characters. Complete your payment when empty.
message Up to 300 characters. A line under the heading. Left out of the page when empty.
amount_label Up to 60 characters. Amount due when empty.
billing_title Up to 60 characters. Billing details when empty.
button_label Up to 40 characters. Pay now when empty.
footer_text Up to 200 characters. Left out of the page when empty.
success_title Up to 120 characters. Payment received when empty.
success_message Up to 300 characters. Left out of the page when empty.
links Up to 3 links in the footer, each {"label": "Terms", "url": "https://example.com/terms"}. A label has up to 30 characters, and an address is https or mailto:.
custom_css Your own stylesheet, up to 20,000 characters.

A colour you choose is a starting point. We work out the other colours of the page from it (muted text, borders, the text on the button) and change a text or link colour that would be hard to read on the colour behind it. Text is plain text, and we escape it on the page. A line break in a one-line text, or a control character in any text, is refused. The three longer texts hold one paragraph, and we turn a line break in them into a space.

The page names a font stack and downloads no font. inter and plex use that font when the customer's device has it, and the closest sans-serif font when it does not.

The page shows the logo from a copy we make: we fetch the address once and place the image in the page itself. A logo we cannot fetch is left out of the page.

custom_css is added after the style above, so it can override anything. We accept style rules and @media and @supports blocks. Everything else is refused: when you send CSS, the request answers 422 with field set to style.custom_css and a message that names the first problem, and nothing is saved. We save the CSS rewritten in a compact form, so it reads back differently from what you sent.

The CSS cannot load anything. We refuse every way a stylesheet could name a file or run code:

  • @import, @font-face, @keyframes, @namespace, @charset and @page.
  • url(), image-set(), image(), element(), cross-fade() and src() in any value. A background or background-image can hold gradients only, so the CSS cannot load fonts or images.
  • expression(), javascript:, behavior, binding, @, <, backslashes and comments inside values and selectors.

We also refuse a property that is not on our list. The families on the list are:

  • Colour: color, accent-color, caret-color and color-scheme.
  • Background: background and its parts, with gradients only for the image.
  • Borders, outlines and corners: border and its parts (not border-image), border-radius and outline.
  • Box model and position: margin, padding, width, height, their minimums and maximums, aspect-ratio, box-sizing, display, position (static, relative, absolute or sticky), top, right, bottom, left, inset, z-index, float, overflow and visibility.
  • Type: font and every font-* property, line-height, letter-spacing, word-spacing, text-align, text-transform, text-decoration, text-overflow, text-indent, white-space, word-break and overflow-wrap.
  • Layout: flex and grid properties, gap, order, align-*, justify-* and place-*.
  • Effects: box-shadow, text-shadow, opacity, transition, transform, cursor, appearance and object-fit.
  • content, which takes a string, none or normal.
  • Custom properties that start with --.

Properties such as animation, filter, list-style and pointer-events are not on the list.

A selector can use element names, classes, ids, attribute selectors, pseudo classes, pseudo elements and combinators. !important is accepted.

The page marks its parts with these classes, and they stay the same between releases:

Class Part of the page
.page The body, which also has .layout-card or .layout-plain and .theme-light or .theme-dark.
.card The box around everything. With the plain layout it has no box.
.logo The logo image.
.heading The heading.
.message The line under the heading.
.amount-box The box that holds the amount.
.amount-label The words before the amount.
.amount The amount.
.form The billing form.
.section-title The form's title.
.field One field with its label. Each also has .field-<name>, such as .field-email.
.label A field's label.
.input A text field.
.select The country list.
.error A message about the form.
.button The pay button, and the buttons on the pages that follow a payment.
.cancel-link The links that lead away from a page: "Cancel and go back", "Return to site" and "Check again".
.footer The footer.
.footer-text The footer's text.
.links The footer's links.
.notice A note on the pages that follow a payment.

The page also sets CSS variables on :root, which your CSS can read with var() or set again: --accent, --on-accent, --bg, --surface, --text, --muted, --faint, --line, --line-strong, --panel, --link, --error, --error-bg, --ring, --radius, --radius-button, --radius-input, --font and --scheme.

JSON
{
  "theme": "dark",
  "accent_color": "#22c55e",
  "background_color": "#0b1020",
  "surface_color": "#131a2e",
  "text_color": "#e6e9f2",
  "radius": "round",
  "font": "inter",
  "layout": "card",
  "logo_url": "https://example.com/assets/logo.png",
  "heading": "Pay your invoice",
  "message": "Enter your billing details. You pay on the next page.",
  "amount_label": "Total due",
  "billing_title": "Billing address",
  "button_label": "Continue to payment",
  "footer_text": "Questions about this invoice? Write to billing@example.com.",
  "success_title": "Thank you",
  "success_message": "We received your payment.",
  "links": [
    {"label": "Terms", "url": "https://example.com/terms"},
    {"label": "Support", "url": "mailto:billing@example.com"}
  ],
  "custom_css": ".card { max-width: 520px; }\n.amount { letter-spacing: .02em; }\n.button { text-transform: uppercase; letter-spacing: .06em; }\n.input:focus { box-shadow: 0 0 0 3px rgba(34, 197, 94, .35); }\n@media (max-width: 520px) { .heading { font-size: 20px; } }"
}

POST /commerce/checkout-pages/preview draws a page with sample data and creates nothing. It takes the commerce.checkout:read scope, and answers 429 with rate_limited beyond 60 previews a minute for the organization.

Field Takes
style Style keys to try, laid over the store's default style as for a page.
amount The sample amount in minor units, 1 to 99,999,999. 2500 by default.
currency_code The sample currency, one of the store's. The store's default currency when left out.
state form, paid, processing, failed, expired or canceled. Any other word draws the form.
Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-pages/preview" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"style": {"theme": "dark", "accent_color": "#22c55e"}, "state": "paid"}' \
  | jq -r .html > preview.html

The answer is {"html": "<!doctype html>..."}, the page as a customer sees it. Open preview.html in a browser. A style the page would refuse answers the same 422 as a page does.

  1. You create a page and send the customer its url.
  2. The customer opens the page, which shows the amount and the billing form in your style.
  3. The customer sends the form. We check it, and the page becomes processing. We hand the amount to your payment site, signed with its secret, as one payment line.
  4. We send the customer to the payment site's own page, and they pay there.
  5. The payment site sends the customer back to your checkout host with its signed result, as Tell the payment site where to send the customer describes. We check the signature and keep the return once.
  6. A payment that succeeded makes the page paid. We send commerce.checkout_page.paid, and the customer goes to your success_url, or to your return_url when you gave no success_url, as Send the customer back describes.

A customer who sends the form again within 10 minutes with the same details goes to the same payment address, and we ask the payment site nothing.

Status What it means
open No customer has started to pay.
processing The customer sent the form, and we are waiting for the payment site's return. A payment the payment site holds for review leaves the page here.
paid The payment site reported a payment for the page's amount and currency.
failed The payment site reported that the payment failed. The customer can open the page and pay again.
canceled You cancelled the page.
expired The page's time passed and nobody is paying on it. A page shows expired in the API as soon as its time has passed. A page does not expire within 10 minutes of a customer's last attempt.

A page takes a payment while it is open, processing or failed. A customer who opens a paid, canceled or expired page sees that, cannot pay, and has a link back to your site.

Late payments, repeats and other amounts

Section titled Late payments, repeats and other amounts

A payment that arrives after the page expired still pays it, when its amount and currency are the page's. Some payments cannot pay a page. We keep them as unmatched in the page's events, and we send commerce.checkout_page.refund_required so you refund them at your payment site. We refund nothing.

reason What happened
canceled The customer paid a page you had cancelled.
second_payment The page was already paid, and the payment site sent another payment for it.
amount_mismatch The payment site reports another amount or currency than the page asked for.
no_page The return names no page of yours.

The same return, passed again, shows what the first one came to and sends no new event.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-pages/chk_01j9k2m4n6p8q0r2s4t6v8w0x2/cancel" \
  -H "X-API-Key: $ORG_API_KEY"

The answer is {"checkout_page": {...}}. You can cancel an open, failed or expired page, and a processing page whose last attempt is older than 30 minutes. Cancelling a cancelled page changes nothing.

Answer Why
409 with payment_in_progress A customer started to pay in the last 30 minutes. Try again later.
409 with checkout_page_unavailable The page is paid.
404 The page is not yours.

Caution

A customer who pays after you cancel a page is still charged by the payment site. We send commerce.checkout_page.refund_required with the reason canceled, and you refund the payment.

Read what the payment site sent back

Section titled Read what the payment site sent back

GET /commerce/checkout-pages/{checkout_page_id}/events lists the signed returns of one page, newest first. It takes limit (1 to 200, 50 by default) and before_id, and answers {"events": [...], "next_before_id": ...}.

JSON
{
  "events": [
    {
      "id": 311,
      "page_id": "chk_01j8z4a7b2c9d3e6f5g8h1k0m4",
      "connector_id": 4,
      "event_id": "maef-98765-succeeded",
      "type": "payment.succeeded",
      "status": "processed",
      "detail": {
        "reference": "maef-parent-98765",
        "parent_order_id": "98765",
        "payment_status": "completed",
        "email": "ada@example.com",
        "amount": 2500,
        "currency_code": "USD",
        "wc_order_id": "2000000087",
        "outcome": "paid"
      },
      "created_at": "2026-10-14T10:02:41Z"
    }
  ],
  "next_before_id": null
}

type is payment.succeeded, payment.pending (held for review) or payment.failed. status is processed when the return changed the page, unmatched when it was a payment that cannot pay the page (detail.reason says why), and ignored when it changed nothing. connector_id is the id of your payment page, and page_id is the page's own chk_ id.

Confirm the payment on your server

Section titled Confirm the payment on your server

Read the page, or take the webhook. The checkout_page and status that we add to your success_url, return_url and cancel_url are in the customer's browser address, so anyone can type them. Never mark an invoice paid from them.

With the API, read the page and check that status is paid:

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/commerce/checkout-pages/chk_01j9k2m4n6p8q0r2s4t6v8w0x2" \
  -H "X-API-Key: $ORG_API_KEY"

With a webhook, subscribe to the three events, or to *, as Receive commerce webhooks describes. Each event has an id that you can use to skip one you handled. resource.id is null: the page is data.checkout_page.

commerce.checkout_page.paid: the page was paid.

JSON
{
  "id": "evt_48311",
  "type": "commerce.checkout_page.paid",
  "livemode": true,
  "created_at": "2026-10-14T10:02:41Z",
  "resource": {"type": "checkout_page", "id": null},
  "data": {"checkout_page": "chk_01j9k2m4n6p8q0r2s4t6v8w0x2", "reference": "INV-1042", "amount": 2500, "currency_code": "USD", "parent_order_id": "98765", "email": "ada@example.com", "livemode": true}
}

commerce.checkout_page.failed: the payment site reported a failed payment.

JSON
{
  "id": "evt_48312",
  "type": "commerce.checkout_page.failed",
  "livemode": true,
  "created_at": "2026-10-14T10:04:09Z",
  "resource": {"type": "checkout_page", "id": null},
  "data": {"checkout_page": "chk_01j9k2m4n6p8q0r2s4t6v8w0x2", "reference": "INV-1042", "amount": 2500, "currency_code": "USD", "parent_order_id": "98766", "email": "ada@example.com", "livemode": true}
}

commerce.checkout_page.refund_required: a payment that cannot pay the page. amount is what the payment site took, which can differ from the page's amount. checkout_page, reference and currency_code can be null when the return named no page.

JSON
{
  "id": "evt_48320",
  "type": "commerce.checkout_page.refund_required",
  "livemode": true,
  "created_at": "2026-10-14T11:20:33Z",
  "resource": {"type": "checkout_page", "id": null},
  "data": {"checkout_page": "chk_01j9k2m4n6p8q0r2s4t6v8w0x2", "reference": "INV-1042", "amount": 2500, "currency_code": "USD", "parent_order_id": "98771", "email": "ada@example.com", "livemode": true, "reason": "second_payment"}
}

GET /commerce/events shows the same events to a caller with commerce.store:read, and shows the email of these three only to a caller who also has commerce.checkout:read.

You refund at your payment site. Nothing here moves money, books anything on your balance, issues an invoice or creates an order, because a hosted checkout page is not a store order.

What Limit
Checkout domains 10 for each organization.
Pages created 600 an hour for each organization.
Domain changes (add and remove) 60 an hour for each organization.
Domain checks 12 an hour for each hostname.
Previews 60 a minute for each organization.
Payment attempts by customers 10 for each page and 30 for each network address in 10 minutes.
Returns from the payment site 600 with a good signature in 10 minutes for each organization. A wrong signature counts against 60 in 10 minutes, and never uses up the 600.
Page life 5 to 43,200 minutes, 1440 by default.

A return that the payment site signed more than 15 minutes ago, and that we have not had before, is not taken. The customer sees that we could not confirm the payment, the page stays as it was, and you check the payment at your payment site.

Situation The customer sees
A mistake in the billing form The form again with what they typed and a message under each field.
Too many attempts "Too many attempts. Try again in a few minutes."
The payment site refused the amount or did not answer "We could not start the payment", with a "Try again" button.
No payment page is turned on for the page's mode and currency any more "This payment page is not available".
A payment still being confirmed "We are confirming your payment", with a "Check again" link, a "Return to site" link and a note not to pay again.
A payment that failed "The payment did not go through", with a "Try again" button and a "Return to site" link.
A payment held on review, or one that cannot pay the page "We received your payment and are checking it. You do not need to pay again." It has a "Return to site" link.
A return we cannot confirm "We could not confirm this payment. If you were charged, contact the seller."
A cancelled or expired page The page says so and tells them to ask the seller for a new payment link. It has a "Return to site" link.
A page that does not exist, or whose store has hosted checkout turned off "Page not found", with no link.
A paid page opened again The receipt, with a "Continue" link to your success_url, or to your return_url when you gave no success_url.
The billing form A "Cancel and go back" link to your cancel_url, or to your return_url when you gave no cancel_url.

When staff turn hosted checkout pages off for a store, the checkout host stops showing forms and taking payments. A return for a processing page is still taken.

Your checkout host opens each page you create in your style, with the amount and the billing form. A customer who pays at your payment site comes back to your host, the page is paid, the customer lands on your success_url or return_url, and you have the commerce.checkout_page.paid event and status: "paid" in the API.

409 with hosted_checkout_not_enabled
Staff have not turned hosted checkout pages on for the store. Ask support.
409 with checkout_domain_required
Add a checkout domain and verify it, as Set up a checkout domain describes.
409 with no_payment_page
The store has no payment page that is turned on for the page's mode and currency. Add or turn on a payment page, and check that its currencies list takes the currency.
409 with verification_failed
We did not find the TXT record. The answer says what we found. Publish _coritan-checkout.<hostname> with the token from records, wait a few minutes, and verify again.
certificate stays pending
The name must point at us before the certificate can be issued. Publish the CNAME record when records lists one, and read the domain again in a few minutes. When records lists none, ask support where to point the name.
422 with field set to style.custom_css
The message names the first rule, property or value we refused. Remove it and send the CSS again. Custom CSS lists what we accept.
The customer paid and the page stays processing
We have not had the payment site's return. Read the page's events: an empty list means no return reached us. Check that the payment site sends customers back to your checkout host and not to your storefront, as Tell the payment site where to send the customer describes. The payment site's own record of the payment shows whether the customer paid.
The page shows "We received your payment and are checking it"
The payment site held the payment for review, or sent a payment that cannot pay the page. Read the page's events and check for commerce.checkout_page.refund_required. Refund an unmatched payment at your payment site.
success_url or return_url opens, but the page is not paid
The address a customer opens proves nothing, and its status can be behind or typed by hand. Read the page with the API, and wait for the return if it is still processing.
422 and detail names ["body", "return_url"]
The request has no return_url. Add the address the customer came from, as Send the customer back describes.
422 with field set to return_url
The address is empty, is not https, is over 2,000 characters, or has spaces or sign-in details. The same limits apply to success_url and cancel_url.
A customer has no link back from a page
Every page that knows its page has one. A page that does not exist, and an error page, have none. Check the page's url for a typing mistake.

API operations on this page

MethodPathWhat it does
GET/api/v1/orgs/{org_slug}/commerce/checkout-pagesList the organization's hosted checkout pages, newest first
POST/api/v1/orgs/{org_slug}/commerce/checkout-pagesMake a hosted checkout page for an amount, and get its address
POST/api/v1/orgs/{org_slug}/commerce/checkout-pages/previewDraw a hosted checkout page with sample data, to see a style before using it
GET/api/v1/orgs/{org_slug}/commerce/checkout-pages/{checkout_page_id}Read one hosted checkout page by its id (chk...)
POST/api/v1/orgs/{org_slug}/commerce/checkout-pages/{checkout_page_id}/cancelCancel a hosted checkout page so it takes no more payments
GET/api/v1/orgs/{org_slug}/commerce/checkout-pages/{checkout_page_id}/eventsList what the parent site reported for one page, newest first
GET/api/v1/orgs/{org_slug}/commerce/checkout-domainsList the organization's checkout domains, oldest first
POST/api/v1/orgs/{org_slug}/commerce/checkout-domainsAdd a hostname to serve checkout pages on
POST/api/v1/orgs/{org_slug}/commerce/checkout-domains/{hostname}/verifyCheck the TXT record now and, when it is there, verify the hostname
DELETE/api/v1/orgs/{org_slug}/commerce/checkout-domains/{hostname}Remove a checkout domain and stop serving pages on it