Skip to content

Create a checkout ​

A checkout represents your order in Maytes. Create it on your server before the customer enters the Maytes-hosted experience. Maytes returns:

  • A checkout_uuid — the order's identifier in Maytes.
  • A checkout_url — where the customer pays.

Create checkouts server-side because the call requires OAuth credentials that must never reach the browser. With the Maytes Checkout Button, the button calls your backend, your backend creates the checkout, and the button launches Maytes.


How the create call fits in ​

StepWhereWhat happens
1. Customer 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. Customer pays in the Maytes windowMaytesFunds are authorized (held).
4. CaptureYour backendYour checkout.authorized webhook handler captures immediately.

See the API reference for complete request and response schemas, errors, and line-item categories.


Why create a checkout? ​

Create sends Maytes the order details needed to:

  • Show the customer what they're paying for. Line items, fees, discounts, tax, and the total populate the split-bill UI that the customer and their friends see.
  • 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.
  • Redirect the customer. return_url and cancel_url tell the Maytes-hosted window where to send the customer after completion or cancellation.
  • 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 as soon as create returns. Every subsequent checkout call needs it:

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 checkout's current statusGET /api/merchant/v1/checkouts/{uuid}

A minimal create request ​

Assumes a maytes client constructed at startup.

ts
const created = await maytes.createCheckout({
  createCheckoutRequest: {
    merchantOrderId: 'order_1001',
    totalAmount: 4500, // minor units — 4500 = AUD 45.00
    currency: 'AUD',
    returnUrl: 'https://shop.example.com/thanks?order=order_1001',
    cancelUrl: 'https://shop.example.com/cart',
    // `LineItem` is a union discriminated on `category`. In TypeScript the
    // object literal IS the variant — set `category` and the right shape is
    // required. Field names are camelCase; the wire format is snake_case.
    items: [
      {
        itemRef: 'GA-1',
        name: 'General Admission',
        quantity: 1,
        unitPrice: 4500,
        currency: 'AUD',
        category: 'other',
      },
    ],
  },
});

// Persist this against your order — everything later keys off it.
const checkoutUuid = created.data.checkoutUuid;
const checkoutUrl = created.data.checkoutUrl;
python
from maytes_api_client.models.create_checkout_request import CreateCheckoutRequest
from maytes_api_client.models.line_item import LineItem
from maytes_api_client.models.other_line_item import OtherLineItem

created = maytes.create_checkout(CreateCheckoutRequest(
    merchant_order_id="order_1001",
    total_amount=4500,               # minor units — 4500 = AUD 45.00
    currency="AUD",
    return_url="https://shop.example.com/thanks?order=order_1001",
    cancel_url="https://shop.example.com/cart",
    # Wrap the CATEGORY VARIANT in LineItem. Passing the fields straight to
    # LineItem(...) is accepted silently and then serialises to `null`.
    items=[LineItem(OtherLineItem(
        item_ref="GA-1",
        name="General Admission",
        quantity=1,
        unit_price=4500,
        currency="AUD",
        category="other",
    ))],
))

# Persist this against your order — everything later keys off it.
checkout_uuid = created.data.checkout_uuid
checkout_url = created.data.checkout_url
php
use MaytesApiClient\Model\CreateCheckoutRequest;
use MaytesApiClient\Model\OtherLineItem;

$created = $maytes->createCheckout(new CreateCheckoutRequest([
    'merchant_order_id' => 'order_1001',
    'total_amount'      => 4500,     // minor units — 4500 = AUD 45.00
    'currency'          => 'AUD',
    'return_url'        => 'https://shop.example.com/thanks?order=order_1001',
    'cancel_url'        => 'https://shop.example.com/cart',
    // Use the category VARIANT, not the base LineItem: the generated parent
    // serialises `category` as its own class name, which the API rejects.
    'items'             => [new OtherLineItem([
        'item_ref'    => 'GA-1',
        'name'        => 'General Admission',
        'quantity'    => 1,
        'unit_price'  => 4500,
        'currency'    => 'AUD',
        'category'    => 'other',
    ])],
]));

// Persist this against your order — everything later keys off it.
$checkoutUuid = $created->getData()->getCheckoutUuid();
$checkoutUrl = $created->getData()->getCheckoutUrl();
go
// Build the category variant, then lift it into the LineItem union.
item := maytes.OtherLineItemAsLineItem(
    maytes.NewOtherLineItem("GA-1", "General Admission", 1, 4500, "AUD", "other"),
)

createReq := *maytes.NewCreateCheckoutRequest(
    4500, "AUD",                     // minor units — 4500 = AUD 45.00
    "https://shop.example.com/thanks?order=order_1001",
    "https://shop.example.com/cart",
    []maytes.LineItem{item},
)
createReq.SetMerchantOrderId("order_1001")

created, _, err := api.DefaultAPI.CreateCheckout(ctx).
    CreateCheckoutRequest(createReq).Execute()
if err != nil { /* handle */ }

// Persist this against your order — everything later keys off it.
checkoutUuid := created.Data.GetCheckoutUuid()
checkoutURL := created.Data.GetCheckoutUrl()
java
import co.maytes.api.model.*;
import java.net.URI;
import java.util.List;

CreateCheckoutResponse created = maytes.createCheckout(
    new CreateCheckoutRequest()
        .merchantOrderId("order_1001")
        .totalAmount(4500)           // minor units — 4500 = AUD 45.00
        .currency("AUD")
        .returnUrl(URI.create("https://shop.example.com/thanks?order=order_1001"))
        .cancelUrl(URI.create("https://shop.example.com/cart"))
        // Wrap the category variant — the base LineItem has no field builders.
        .items(List.of(new LineItem(new OtherLineItem()
            .itemRef("GA-1")
            .name("General Admission")
            .quantity(1)
            .unitPrice(4500)
            .currency("AUD")
            .category(OtherLineItem.CategoryEnum.OTHER)))));

// Persist this against your order — everything later keys off it.
String checkoutUuid = created.getData().getCheckoutUuid();
String checkoutUrl = created.getData().getCheckoutUrl();
ruby
created = MAYTES.create_checkout(
  MaytesApiClient::CreateCheckoutRequest.new(
    merchant_order_id: 'order_1001',
    total_amount: 4500,              # minor units — 4500 = AUD 45.00
    currency: 'AUD',
    return_url: 'https://shop.example.com/thanks?order=order_1001',
    cancel_url: 'https://shop.example.com/cart',
    # Pass the category variant directly; LineItem is a oneOf wrapper.
    items: [MaytesApiClient::OtherLineItem.new(
      item_ref: 'GA-1',
      name: 'General Admission',
      quantity: 1,
      unit_price: 4500,
      currency: 'AUD',
      category: 'other',
    )],
  ),
)

# Persist this against your order — everything later keys off it.
checkout_uuid = created.data.checkout_uuid
checkout_url = created.data.checkout_url
csharp
using MaytesApiClient.Model;

var created = await maytes.CreateCheckoutAsync(new CreateCheckoutRequest(
    totalAmount: 4500,               // minor units — 4500 = AUD 45.00
    currency: "AUD",
    returnUrl: "https://shop.example.com/thanks?order=order_1001",
    cancelUrl: "https://shop.example.com/cart",
    // Wrap the category variant — LineItem's constructors take variants only.
    items: new List<LineItem>
    {
        new LineItem(new OtherLineItem(
            itemRef: "GA-1", name: "General Admission", quantity: 1,
            unitPrice: 4500, currency: "AUD",
            category: OtherLineItem.CategoryEnum.Other)),
    })
{
    MerchantOrderId = "order_1001",
});

// Persist this against your order — everything later keys off it.
var checkoutUuid = created.Data.CheckoutUuid;
var checkoutUrl = created.Data.CheckoutUrl;

Response:

json
{
  "data": {
    "checkout_uuid": "019fbb9e-ecd1-7059-99f3-f8bf47b63931",
    "status": "created",
    "checkout_url": "https://checkout.maytes.co/?id=019fbb9e-ecd1-7059-99f3-f8bf47b63931",
    "expires_at": null
  }
}

The required fields are total_amount, currency, return_url, cancel_url, and at least one item. Optional fields include merchant_order_id, merchant_expires_at, fees, discounts, tax, shipping_amount, customer_data, and metadata. See the API reference for schemas 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) — your merchant_expires_at, echoed in UTC, or null if you did not send one. The hosted page shows it to the buyer as the deadline.
  • Independently of that, an unpaid checkout is kept for one hour after creation. After whichever comes first, the checkout cannot be completed; create a new checkout for another attempt.
  • merchant_expires_at (optional request field, RFC 3339 datetime) — your order deadline. Use it for time-boxed sales such as ticket holds or reservation windows. Any offset is accepted (2026-09-14T13:30:00+10:00 or 2026-09-14T03:30:00Z); the offset is required. We always return it in UTC, e.g. 2026-09-14T03:30:00.000Z.

An uncaptured authorization is automatically voided after 2 minutes. See the capture window.


The two redirect URLs ​

Maytes stores return_url (success) and cancel_url (abandonment) verbatim and passes them to the Maytes window. We do not insert the checkout_uuid.

return_url is a confirmation page, not a fulfillment trigger

In the webhook flow, fulfill on capture success and use return_url only for the customer experience. Capture occurs before the customer leaves Maytes; they may remain in the split step or close the window without reaching return_url, but the payment still stands. See when to capture.

Correlating the return ​

Include your merchant_order_id in the return_url query string so the success page can identify the order:

return_url: https://shop.example.com/thanks?order=order_1001

When the customer lands on /thanks?order=order_1001, use order_1001 to find the saved checkout_uuid. Then GET the checkout and branch on status; in the webhook flow, it will already be captured.


How the customer reaches Maytes ​

Add the Maytes Checkout Button to your page. On click, it calls your backend to create a checkout and opens the hosted checkout: in a centered popup with a full-screen overlay on wide screens (the default), or in the current tab on phones, when the browser blocks the popup, or with mode: 'redirect'. Return checkout_uuid and checkout_url from your backend; the button opens the URL you return.

Option B — Redirect to checkout_url directly ​

To avoid page-level JavaScript, redirect the customer's browser to the returned checkout_url:

js
res.redirect(created.data.checkout_url);

The customer pays in the Maytes-hosted window, then returns to your return_url or cancel_url. This option does not provide the popup and overlay experience.

Option C — from your Stripe Payment Element ​

If your checkout already runs on Stripe, offer Split with Maytes as a Stripe custom payment method. Selecting it hands control back to your page, which calls your backend to create the checkout and redirects to checkout_url. See Maytes in your Stripe Payment Element, or the simpler express row placement.


Amount rules ​

Maytes recomputes the order from its items and compares the result with total_amount. A mismatch returns 422 invalid_amount_arithmetic.

The formula:

text
total_amount  =  Σ(item.unit_price × item.quantity)
              −  Σ(discounts)
              +  shipping_amount
              +  Σ(fees)
              +  tax (when not inclusive)

Send every amount as an integer in minor units. If your cart uses floats, round to whole minor units first. The tax object accepts an amount in minor units, an optional percentage, and an inclusive flag. When inclusive is true, tax is already included in item prices and does not increase the total.


Line-item categories at a glance ​

Every item has a category. Some categories require custom_data; for example, ticket requires event_id, event_start_at, and timezone:

categoryRequires custom_dataNotes
ticket✅ event_id, event_start_at, timezone (+ optional fields)Events, gigs, sports
physical_goods✅ product_idAnything shipped
digital_goodsoptionalDownloads, in-game items
serviceoptionalBookings, time-based services
food_beverage—F&B orders
donation—Charitable contributions
other—Anything that doesn't fit

See the API reference for each category's request variant, required fields, and optional fields.

custom_data isn't in the SDK yet

The SDK's LineItem does not yet include custom_data, so send ticket and physical_goods carts to POST /api/merchant/v1/checkouts as raw JSON. See the API reference. You can still use the SDK for capture, cancel, status, and webhook verification. The SDK supports categories that do not require custom_data: other, food_beverage, donation, and digital_goods or service when no custom data is needed.


Common pitfalls ​

Don't recreate on conflict

Retrying create for an order already in progress returns 409 order_already_authorized. Do not create another checkout for the same merchant_order_id; retrieve the existing checkout_uuid from your store and continue with it.

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.
Fulfillment wired to the return_url landingIn the webhook flow that landing may never happen. Fulfill on capture.
Treating return_url as templatedIt isn't. Carry your own correlation key (see above).

What's next ​