Appearance
Create a checkout
A checkout is your order's representation inside Maytes. You create it on your server before the shopper enters the Maytes-hosted experience, so we have everything we need to show them the right items, the right total, and to bring them back to the right page when they're done. In return, Maytes gives you two things you keep using for the rest of the order's life:
- A
checkout_uuid— your handle for this order in Maytes' system. - A
checkout_url— where the shopper goes to pay.
Creating a checkout is always a server-side call — it needs your OAuth credentials, which never go to the browser. With the Maytes Checkout Button you don't create it as a separate step: the button calls your backend on click, your backend creates the checkout, and the button opens the Maytes popup itself.
How the create call fits in
| Step | Where | What happens |
|---|---|---|
| 1. Shopper clicks the button | Your page | The button invokes your createCheckout callback. |
| 2. Create the checkout | Your backend | Your endpoint calls POST /api/merchant/v1/checkouts with the cart and the URLs, and returns the checkout_uuid. |
| 3. Shopper pays in the Maytes window | Maytes | Funds are authorized (held). |
| 4. Capture | Your backend | In the webhook flow, your checkout.authorized handler captures immediately. In the redirect flow, your success page captures after the shopper lands on return_url. |
The exact request/response schemas — every field, every error, every line-item category — live in the API reference. This page is the narrative for why you call create, what to put in it, and what to do with the response.
Why create a checkout?
The create call is how you hand the order over to Maytes. We need the details up front so we can:
- Show the shopper what they're paying for. The line items, fees, discounts, tax, and the total power the split-bill UI in the Maytes window — they're what the shopper's friends see when they accept their share.
- Validate the order math. Maytes reconciles the items + fees + discounts + tax against
total_amount; if it doesn't add up we reject with422 invalid_amount_arithmetic. See Amount rules below. - Know where to send the shopper after.
return_urlandcancel_urlare where the Maytes-hosted window redirects the shopper when they finish or back out. - Tie our checkout to your order. Pass
merchant_order_idso you can reconcile against your store later, and so we can reject duplicate create attempts for the same order with409 order_already_authorized.
What you do with checkout_uuid
Persist it against your order the moment create returns. You'll need it for every subsequent call:
| Later step | Endpoint |
|---|---|
| Capture the payment | POST /api/merchant/v1/checkouts/{uuid}/capture |
| Cancel before capture (release the auth) | POST /api/merchant/v1/checkouts/{uuid}/cancel |
| Read the order's current status | GET /api/merchant/v1/checkouts/{uuid} |
A minimal create request
bash
curl -s https://api.maytes.co/api/merchant/v1/checkouts \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H 'content-type: application/json' \
-d '{
"merchant_order_id": "order_1001",
"total_amount": 4500,
"currency": "AUD",
"return_url": "https://shop.example.com/thanks?order=order_1001",
"cancel_url": "https://shop.example.com/cart",
"items": [
{
"item_ref": "GA-1",
"name": "General Admission",
"quantity": 1,
"unit_price": 4500,
"currency": "AUD",
"category": "ticket",
"custom_data": {
"event_id": "evt_123",
"event_start_at": "2026-12-05T19:00:00Z",
"timezone": "Australia/Sydney"
}
}
]
}'Response:
json
{
"data": {
"checkout_uuid": "0192a8f2-...",
"status": "created",
"checkout_url": "https://checkout.maytes.co/0192a8f2-...",
"expires_at": "2026-09-14T03:30:00.000Z"
}
}The five required fields are total_amount, currency, return_url, cancel_url, and at least one item. Everything else — merchant_order_id, merchant_expires_at, fees, discounts, tax, shipping_amount, customer_data, metadata — is optional but useful. The API reference shows every field with its schema and examples.
Amounts are minor units
All monetary fields are integers in the currency's minor unit. 4500 = AUD 45.00. Never send decimals.
Checkout expiry
expires_at(response) — the checkout session's deadline. If the shopper hasn't paid by then, the checkout can no longer be completed; create a fresh one for a new attempt.merchant_expires_at(optional request field, ISO datetime) — your own deadline for the order. Use it when the thing being sold is time-boxed (ticket holds, reservation windows) so an order the shopper never finishes doesn't linger past the point where you'd honour it.
Separately, an authorization that nothing captures is automatically voided after 2 minutes — see the capture window.
The two redirect URLs
return_url (success) and cancel_url (abandonment) are stored verbatim and handed to the Maytes window as-is — we do not substitute the checkout_uuid into them.
return_url is a confirmation page, not a fulfilment trigger
In the webhook flow, capture happens from your webhook handler before the shopper leaves Maytes — and the shopper may spend minutes in the split step afterwards, or close the window and never land on return_url at all. Their payment stands either way. Fulfil on capture success; treat the return_url landing as UX only. (In the legacy redirect flow, the landing is your capture signal — see when to capture.)
Correlating the return
At the moment the shopper lands on your success page, the page needs a way to know which order they're returning for. The simplest pattern: put your own merchant_order_id (which you control before create even returns) in the URL query string.
return_url: https://shop.example.com/thanks?order=order_1001When the shopper lands on /thanks?order=order_1001, look up order_1001 → the checkout_uuid you saved, then GET the checkout and branch on status — in the webhook flow it will already be captured.
How the shopper reaches Maytes
Option A — Maytes Checkout Button (recommended)
The Maytes Checkout Button is a small JS component that drops onto your page. On click it calls your backend to create the checkout, then opens the Maytes-hosted window in a popup (it derives the URL from its environment — you don't pass checkout_url), shows a full-screen overlay on your page while the shopper pays, and clears when the popup closes. Popup-blocked browsers fall back to a same-window redirect automatically.
Option B — Redirect to checkout_url directly
If you'd rather not run JavaScript on the page, redirect the shopper's browser to the checkout_url we returned:
js
res.redirect(created.data.checkout_url);The shopper completes payment in the Maytes-hosted window and is redirected to your return_url (or cancel_url) when done. Simpler, but you lose the popup + overlay UX.
Amount rules
Maytes recomputes the order from the items and checks it against the total_amount you sent. If they don't match we reject the create with 422 invalid_amount_arithmetic — the most common failure on this endpoint.
The formula:
text
total_amount = Σ(item.unit_price × item.quantity)
− Σ(discounts)
+ shipping_amount
+ Σ(fees)
+ tax (when not inclusive)Because every amount is an integer in minor units there's no rounding ambiguity. If you build totals in your cart with floats, round to whole minor units before sending. The tax object takes an amount (minor units), an optional percentage, and an inclusive flag — when inclusive is true the tax is already inside the item prices and doesn't add to the total.
Line-item categories at a glance
Every item carries a category that tells Maytes what kind of thing it is. Some categories require a custom_data block — for example a ticket needs event_id, event_start_at, and timezone:
category | Requires custom_data | Notes |
|---|---|---|
ticket | ✅ event_id, event_start_at, timezone (+ optional fields) | Events, gigs, sports |
physical_goods | ✅ product_id | Anything shipped |
digital_goods | optional | Downloads, in-game items |
service | optional | Bookings, time-based services |
food_beverage | — | F&B orders |
donation | — | Charitable contributions |
other | — | Anything that doesn't fit |
The full per-category schema (with optional fields) is in the API reference — each category appears as its own request variant.
Common pitfalls
Don't recreate on conflict
If you retry a create for an order that's already in progress, you get 409 order_already_authorized. Don't create a second checkout for the same merchant_order_id — fetch the existing checkout_uuid from your own store and resume from there.
| Pitfall | Fix |
|---|---|
| Amounts as floats / strings | Always integers, minor units. |
| Cart currency doesn't match each item's currency | Every item must use the same currency as the cart total. |
Reusing item_ref across two items in the same cart | item_ref is unique within a cart. |
Forgetting event_id / event_start_at on category: "ticket" | These are required for ticket items; the API returns 422. |
Fulfilment wired to the return_url landing | In the webhook flow that landing may never happen. Fulfil on capture. |
Treating return_url as templated | It isn't. Carry your own correlation key (see above). |
What's next
- Wire up Webhooks — the
checkout.authorizedevent that tells you to capture. - Open the Maytes window with the Maytes Checkout Button.
- Finalise the order with Capture & order status.
- Browse every field and error in the API reference.
