---
url: /checkout-button.md
---
# Maytes Checkout Button

One button on your existing checkout. Your customer pays their share of the order on Maytes, and invites their mates to pay theirs.

## How it works

1. The customer clicks Split with Maytes on your page, and the button calls your `createCheckout`.
2. Your server creates the checkout and returns `checkout_uuid` and `checkout_url`.
3. The customer pays their share on Maytes, in a popup or the same tab.
4. Maytes sends `checkout.authorized` to your webhook, and your server captures.

::: details See the full sequence

```mermaid
sequenceDiagram
  autonumber
  participant P as Your page, the button
  participant Y as Your server
  participant K as Maytes
  P->>Y: Customer clicks, button calls createCheckout()
  Y->>K: POST /api/merchant/v1/checkouts (OAuth)
  K-->>Y: checkout_uuid, checkout_url
  Y-->>P: the checkout id and URL
  P->>K: Button opens the hosted checkout (popup or same tab)
  K->>Y: Webhook: checkout.authorized
  Y->>K: POST /checkouts/{uuid}/capture
  K-->>P: Customer is sent to your return_url
```

:::

## Choose how Maytes appears

The Split with Maytes button can stand on its own or sit inside your Stripe checkout. Every placement creates the checkout on your server the same way.

| Placement | Use it when | Guide |
|---|---|---|
| **On its own** | Maytes is a pay button of its own on your page | [Add the button](#add-the-button), below |
| **In Stripe's express row** | Your checkout shows Apple Pay, Google Pay and Link, and Maytes goes in a full-width row under them | [Maytes in your Stripe express row](/stripe-express-checkout) |
| **In Stripe's Payment Element** | Maytes is a payment method in your Stripe form, with the Maytes button in place of your Pay button | [Maytes in your Stripe Payment Element](/stripe) |

## Add the button

Every stack calls your server through one small function. `createMaytesCheckout` asks your server to create the checkout and returns what the button needs:

```js
async function createMaytesCheckout(cartId) {
  const res = await fetch('/api/maytes/create-checkout', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ cartId }),
  });
  if (!res.ok) throw new Error('Could not create the Maytes checkout');
  const { checkout_uuid, checkout_url } = await res.json();
  return { checkoutId: checkout_uuid, checkoutUrl: checkout_url };
}
```

`/api/maytes/create-checkout` is a route on **your** server; name it anything. It calls Maytes's `POST /api/merchant/v1/checkouts` with your API credentials, which must never reach the browser. See [Create the checkout on your server](#create-the-checkout-on-your-server).

Pick your stack:

::: code-group

```html [HTML]
<div id="maytes-button"></div>

<script
  src="https://js.maytes.co/v1/checkout-button.js"
  crossorigin="anonymous"
></script>
<script>
  const maytes = window.Maytes({
    environment: 'sandbox',
    createCheckout: () => createMaytesCheckout('cart_8fd21'),
  });
  const slot = document.getElementById('maytes-button');
  maytes.renderButton(slot, { block: true });
</script>
```

```ts [npm]
// npm install @maytes/checkout-button
import { Maytes } from '@maytes/checkout-button';

const maytes = Maytes({
  environment: 'sandbox',
  createCheckout: () => createMaytesCheckout('cart_8fd21'),
});

const slot = document.getElementById('maytes-button')!;
maytes.renderButton(slot, { block: true });
// When the page goes away: maytes.destroy();
```

```tsx [React]
// npm install @maytes/checkout-button
import {
  MaytesProvider,
  MaytesButton,
} from '@maytes/checkout-button/react';

export function SplitWithMaytes({ cartId }: { cartId: string }) {
  return (
    <MaytesProvider
      environment="sandbox"
      createCheckout={() => createMaytesCheckout(cartId)}
    >
      <MaytesButton block />
    </MaytesProvider>
  );
}
```

```tsx [Next.js]
'use client'; // createCheckout is a function, so this runs in the browser
import {
  MaytesProvider,
  MaytesButton,
} from '@maytes/checkout-button/react';

export function SplitWithMaytes({ cartId }: { cartId: string }) {
  return (
    <MaytesProvider
      environment="sandbox"
      createCheckout={() => createMaytesCheckout(cartId)}
    >
      <MaytesButton block />
    </MaytesProvider>
  );
}
```

```vue [Vue]
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { Maytes, type MaytesSDK } from '@maytes/checkout-button';

const props = defineProps<{ cartId: string }>();
const slot = ref<HTMLDivElement>();
let maytes: MaytesSDK | undefined;

onMounted(() => {
  maytes = Maytes({
    environment: 'sandbox',
    createCheckout: () => createMaytesCheckout(props.cartId),
  });
  maytes.renderButton(slot.value!, { block: true });
});
onBeforeUnmount(() => maytes?.destroy());
</script>

<template><div ref="slot" /></template>
```

```ts [Angular]
import {
  AfterViewInit,
  Component,
  ElementRef,
  Input,
  OnDestroy,
  ViewChild,
} from '@angular/core';
import { Maytes, type MaytesSDK } from '@maytes/checkout-button';

@Component({
  selector: 'split-with-maytes',
  standalone: true,
  template: '<div #slot></div>',
})
export class SplitWithMaytesComponent
  implements AfterViewInit, OnDestroy
{
  @Input() cartId = '';
  @ViewChild('slot') slot!: ElementRef<HTMLDivElement>;
  private maytes?: MaytesSDK;

  ngAfterViewInit() {
    this.maytes = Maytes({
      environment: 'sandbox',
      createCheckout: () => createMaytesCheckout(this.cartId),
    });
    this.maytes.renderButton(this.slot.nativeElement, { block: true });
  }

  ngOnDestroy() {
    this.maytes?.destroy();
  }
}
```

```svelte [Svelte]
<script lang="ts">
  import { onMount } from 'svelte';
  import { Maytes } from '@maytes/checkout-button';

  let { cartId }: { cartId: string } = $props();
  let slot: HTMLDivElement;

  // onMount runs only in the browser, so SvelteKit's SSR is safe.
  onMount(() => {
    const maytes = Maytes({
      environment: 'sandbox',
      createCheckout: () => createMaytesCheckout(cartId),
    });
    maytes.renderButton(slot, { block: true });
    return () => maytes.destroy();
  });
</script>

<div bind:this={slot}></div>
```

:::

### Button options

`renderButton(slot, options)` and `<MaytesButton>` take these options. All are optional.

| Option | Default | What it does |
|---|---|---|
| `block` | `false` | Stretches the button to the full width of its container. |
| `mode` | `'popup'` | `'popup'` opens Maytes in a window over your page; `'redirect'` opens it in the same tab. Phones always use the same tab. |
| `label` | `'Split with'` | The text before the Maytes logo. |
| `radius` | pill | Corner radius in pixels, 0 up to half the height. |
| `height` | natural height | Height in pixels, 40 to 55. Keeps the label on one line. Use `{ radius: 6, height: 48 }` to match Apple Pay and Google Pay. |
| `redirectOverlay` | `false` | Shows the Maytes loading screen while a same-tab checkout is created. |

In React, pass them as props: `<MaytesButton block mode="redirect" />`. Events, `openCheckout()` and CSS custom properties are in the [API reference](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/api.md).

The React components (`@maytes/checkout-button/react`) need version 1.2 or later. They work with React 18 and 19, StrictMode and server rendering, and from your own Pay button through `useMaytes()`: see the [React guide](https://github.com/kttipay/maytes-checkout-button/blob/main/docs/guides/react.md).

Full setup guides for every stack, plus the button's options and events, are in the [SDK README](https://github.com/kttipay/maytes-checkout-button#readme).

## Create the checkout on your server

::: warning Keep credentials on your server
Price the cart from your own catalogue and save `checkout_uuid` against the order before you respond. Your `client_id` and `client_secret` never reach the browser.
:::

::: code-group

```ts [TypeScript]
import { createMaytesApiClient, ResponseError } from '@maytes/api-client-js';

const maytesApi = createMaytesApiClient({
  endpoint: 'https://api.maytes.co',
  clientId: process.env.MAYTES_CLIENT_ID!,
  clientSecret: process.env.MAYTES_CLIENT_SECRET!,
});

app.post('/api/maytes/create-checkout', async (req, res) => {
  const cart = await loadCart(req.body.cartId);
  const merchantOrderId = await createPendingOrder(cart);
  try {
    const created = await maytesApi.createCheckout({
      createCheckoutRequest: {
        merchantOrderId,
        totalAmount: cart.total, // minor units
        currency: 'AUD',
        returnUrl: `https://shop.example.com/thanks?order=${merchantOrderId}`,
        cancelUrl: 'https://shop.example.com/cart',
        items: cart.lines.map((line) => ({
          itemRef: line.sku, name: line.name, quantity: line.quantity,
          unitPrice: line.unitPrice, currency: 'AUD', category: 'other',
        })),
      },
    });
    await saveCheckoutUuid(merchantOrderId, created.data.checkoutUuid);
    res.json({ checkout_uuid: created.data.checkoutUuid, checkout_url: created.data.checkoutUrl });
  } catch (err) {
    if (err instanceof ResponseError) return res.status(502).json({ error: 'checkout_create_failed' });
    throw err;
  }
});
```

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

# In your POST /api/maytes/create-checkout handler, with `maytes` built at startup:
created = maytes.create_checkout(CreateCheckoutRequest(
    merchant_order_id=order_id,
    total_amount=total,              # minor units
    currency="AUD",
    return_url=f"https://shop.example.com/thanks?order={order_id}",
    cancel_url="https://shop.example.com/cart",
    items=[LineItem(OtherLineItem(
        item_ref="GA-1", name="General Admission", quantity=1,
        unit_price=total, currency="AUD", category="other",
    ))],
))
save_checkout_uuid(order_id, created.data.checkout_uuid)
return {"checkout_uuid": created.data.checkout_uuid, "checkout_url": created.data.checkout_url}
```

```php [PHP]
use MaytesApiClient\Model\CreateCheckoutRequest;
use MaytesApiClient\Model\OtherLineItem;

// In your POST /api/maytes/create-checkout endpoint, with $maytes built at startup:
$created = $maytes->createCheckout(new CreateCheckoutRequest([
    'merchant_order_id' => $orderId,
    'total_amount'      => $total,   // minor units
    'currency'          => 'AUD',
    'return_url'        => "https://shop.example.com/thanks?order={$orderId}",
    'cancel_url'        => 'https://shop.example.com/cart',
    'items'             => [new OtherLineItem([
        'item_ref' => 'GA-1', 'name' => 'General Admission', 'quantity' => 1,
        'unit_price' => $total, 'currency' => 'AUD', 'category' => 'other',
    ])],
]));
saveCheckoutUuid($orderId, $created->getData()->getCheckoutUuid());
echo json_encode([
    'checkout_uuid' => $created->getData()->getCheckoutUuid(),
    'checkout_url'  => $created->getData()->getCheckoutUrl(),
]);
```

```go [Go]
// In your POST /api/maytes/create-checkout handler, with `api` built at startup:
item := maytes.OtherLineItemAsLineItem(
    maytes.NewOtherLineItem("GA-1", "General Admission", 1, total, "AUD", "other"),
)
createReq := *maytes.NewCreateCheckoutRequest(
    total, "AUD", // minor units
    "https://shop.example.com/thanks?order="+orderID,
    "https://shop.example.com/cart",
    []maytes.LineItem{item},
)
createReq.SetMerchantOrderId(orderID)

created, _, err := api.DefaultAPI.CreateCheckout(ctx).CreateCheckoutRequest(createReq).Execute()
if err != nil {
    http.Error(w, "checkout_create_failed", http.StatusBadGateway)
    return
}
saveCheckoutUUID(orderID, created.Data.GetCheckoutUuid())
json.NewEncoder(w).Encode(map[string]string{
    "checkout_uuid": created.Data.GetCheckoutUuid(),
    "checkout_url":  created.Data.GetCheckoutUrl(),
})
```

```java [Java]
// In your POST /api/maytes/create-checkout controller, with `maytes` built at startup:
CreateCheckoutResponse created = maytes.createCheckout(
    new CreateCheckoutRequest()
        .merchantOrderId(orderId)
        .totalAmount(total)           // minor units
        .currency("AUD")
        .returnUrl(URI.create("https://shop.example.com/thanks?order=" + orderId))
        .cancelUrl(URI.create("https://shop.example.com/cart"))
        .items(List.of(new LineItem(new OtherLineItem()
            .itemRef("GA-1").name("General Admission").quantity(1)
            .unitPrice(total).currency("AUD")
            .category(OtherLineItem.CategoryEnum.OTHER)))));

saveCheckoutUuid(orderId, created.getData().getCheckoutUuid());
return Map.of(
    "checkout_uuid", created.getData().getCheckoutUuid(),
    "checkout_url", created.getData().getCheckoutUrl());
```

```ruby [Ruby]
# In your POST /api/maytes/create-checkout handler, with MAYTES built at startup:
created = MAYTES.create_checkout(
  MaytesApiClient::CreateCheckoutRequest.new(
    merchant_order_id: order_id,
    total_amount: total,             # minor units
    currency: 'AUD',
    return_url: "https://shop.example.com/thanks?order=#{order_id}",
    cancel_url: 'https://shop.example.com/cart',
    items: [MaytesApiClient::OtherLineItem.new(
      item_ref: 'GA-1', name: 'General Admission', quantity: 1,
      unit_price: total, currency: 'AUD', category: 'other',
    )],
  ),
)
save_checkout_uuid(order_id, created.data.checkout_uuid)
{ checkout_uuid: created.data.checkout_uuid, checkout_url: created.data.checkout_url }.to_json
```

```csharp [.NET]
// In your POST /api/maytes/create-checkout endpoint, with `maytes` built at startup:
var created = await maytes.CreateCheckoutAsync(new CreateCheckoutRequest(
    totalAmount: total,              // minor units
    currency: "AUD",
    returnUrl: $"https://shop.example.com/thanks?order={orderId}",
    cancelUrl: "https://shop.example.com/cart",
    items: new List<LineItem>
    {
        new LineItem(new OtherLineItem(
            itemRef: "GA-1", name: "General Admission", quantity: 1,
            unitPrice: total, currency: "AUD",
            category: OtherLineItem.CategoryEnum.Other)),
    })
{
    MerchantOrderId = orderId,
});

await SaveCheckoutUuid(orderId, created.Data.CheckoutUuid);
return Results.Json(new { checkout_uuid = created.Data.CheckoutUuid, checkout_url = created.Data.CheckoutUrl });
```

:::

Every request field is on [Create a checkout](/create-checkout#a-minimal-create-request).

## Capture on the webhook

Your page never learns the payment result. When Maytes sends [`checkout.authorized`](/webhooks), [capture](/capture-and-status#when-to-call-capture) within 2 minutes, then show the order on your `return_url` page.

## Go live

| `environment` | Opens | Use with |
|---|---|---|
| `sandbox` | The [sandbox](/sandbox-environment) checkout | Sandbox API credentials |
| `production` | The live checkout | Production credentials |

1. Set the button to `environment: 'production'`.
2. Point your server at your production credentials.
3. Subscribe your production webhook, and capture on it.

The button's environment must match your server's. Return the `checkout_url` Maytes gives you; never build one.

## Other ways to send customers to Maytes

| Instead of the button | Use when |
|---|---|
| [Redirect to `checkout_url`](/create-checkout#option-b-—-redirect-to-checkout-url-directly) | You don't want JavaScript on the page |

Something wrong with the button? Open an issue on [the SDK repository](https://github.com/kttipay/maytes-checkout-button/issues), or email support@maytes.co.
