---
url: /create-checkout.md
---
# 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](/checkout-button), the button calls your backend, your backend creates the checkout, and the button launches Maytes.

***

## How the create call fits in

| Step                                      | Where        | What happens                                                                                                                                                                                                           |
| ----------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. **Customer 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`](/api-reference#api-1/tag/checkout-merchant/POST/api/merchant/v1/checkouts) with the cart and the URLs, and returns the `checkout_uuid`.                        |
| 3. **Customer pays in the Maytes window** | Maytes       | Funds are authorized (held).                                                                                                                                                                                           |
| 4. **Capture**                            | Your backend | Your `checkout.authorized` [webhook](/how-it-works#the-webhook-flow) handler captures immediately.                                                                                                                  |

See the **[API reference](/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](#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 step                               | Endpoint                                                                                                                                             |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Capture the payment                      | [`POST /api/merchant/v1/checkouts/{uuid}/capture`](/api-reference#api-1/tag/checkout-merchant/POST/api/merchant/v1/checkouts/{checkoutUuid}/capture) |
| Cancel before capture (release the auth) | [`POST /api/merchant/v1/checkouts/{uuid}/cancel`](/api-reference#api-1/tag/checkout-merchant/POST/api/merchant/v1/checkouts/{checkoutUuid}/cancel)   |
| Read the checkout's current status       | [`GET /api/merchant/v1/checkouts/{uuid}`](/api-reference#api-1/tag/checkout-merchant/GET/api/merchant/v1/checkouts/{checkoutUuid})                   |

***

## A minimal create request

Assumes a `maytes` client [constructed at startup](/build-your-integration#construct-the-client).

::: code-group

```ts [TypeScript]
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 [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 [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 [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 [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 [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 [.NET]
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](/api-reference)** for schemas and examples.

::: tip 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](/capture-and-status#when-to-call-capture).

***

## 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`.

::: warning `return_url` is a confirmation page, not a fulfillment trigger
In the [webhook flow](/how-it-works#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](/capture-and-status#when-to-call-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

### Option A — Maytes Checkout Button (recommended)

Add the [Maytes Checkout Button](/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](/stripe), or the simpler [express row](/stripe-express-checkout) 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`:

| `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     |

See the [API reference](/api-reference) for each category's request variant, required fields, and optional fields.

::: warning `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](/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

::: warning 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.
:::

| 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.                                     |
| Fulfillment wired to the `return_url` landing                    | In the webhook flow that landing may never happen. Fulfill on [capture](/capture-and-status). |
| Treating `return_url` as templated                               | It isn't. Carry your own correlation key (see [above](#correlating-the-return)).              |

***

## What's next

* Wire up [Webhooks](/webhooks) — the `checkout.authorized` event that tells you to capture.
* Open the Maytes window with the [Maytes Checkout Button](/checkout-button).
* Finalize the order with [Capture & checkout status](/capture-and-status).
* Browse every field and error in the [API reference](/api-reference).
