Skip to content

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

StepWhereWhat happens
1. Shopper clicks the buttonYour pageThe button invokes your createCheckout callback.
2. Create the checkoutYour backendYour endpoint calls POST /api/merchant/v1/checkouts with the cart and the URLs, and returns the checkout_uuid.
3. Shopper pays in the Maytes windowMaytesFunds are authorized (held).
4. CaptureYour backendIn 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 with 422 invalid_amount_arithmetic. See Amount rules below.
  • Know where to send the shopper after. return_url and cancel_url are where the Maytes-hosted window redirects the shopper when they finish or back out.
  • Tie our checkout to your order. Pass merchant_order_id so you can reconcile against your store later, and so we can reject duplicate create attempts for the same order with 409 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 stepEndpoint
Capture the paymentPOST /api/merchant/v1/checkouts/{uuid}/capture
Cancel before capture (release the auth)POST /api/merchant/v1/checkouts/{uuid}/cancel
Read the order's current statusGET /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_1001

When 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

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:

categoryRequires custom_dataNotes
ticketevent_id, event_start_at, timezone (+ optional fields)Events, gigs, sports
physical_goodsproduct_idAnything shipped
digital_goodsoptionalDownloads, in-game items
serviceoptionalBookings, time-based services
food_beverageF&B orders
donationCharitable contributions
otherAnything 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.

PitfallFix
Amounts as floats / stringsAlways integers, minor units.
Cart currency doesn't match each item's currencyEvery item must use the same currency as the cart total.
Reusing item_ref across two items in the same cartitem_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 landingIn the webhook flow that landing may never happen. Fulfil on capture.
Treating return_url as templatedIt isn't. Carry your own correlation key (see above).

What's next