Skip to content

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.

Building with an AI agent? Give it these docs and our agent skill

Try it ​

How it works ​

  1. Customer clicks Split with MaytesYour page
  2. Your server creates the checkoutYour server
  3. Customer pays their shareMaytes
  4. Your webhook captures the paymentYour server
See the full sequence

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.

PlacementUse it whenGuide
On its ownMaytes is a pay button of its own on your pageAdd the button, below
In Stripe's express rowYour checkout shows Apple Pay, Google Pay and Link, and Maytes goes in a full-width row under themMaytes in your Stripe express row
In Stripe's Payment ElementMaytes is a payment method in your Stripe form, with the Maytes button in place of your Pay buttonMaytes in your Stripe Payment Element

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.

Pick your stack:

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 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
// 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
'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
<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
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
<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.

OptionDefaultWhat it does
blockfalseStretches 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.
radiuspillCorner radius in pixels, 0 up to half the height.
heightnatural heightHeight in pixels, 40 to 55. Keeps the label on one line. Use { radius: 6, height: 48 } to match Apple Pay and Google Pay.
redirectOverlayfalseShows 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.

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.

Full setup guides for every stack, plus the button's options and events, are in the SDK README.

Create the checkout on your server ​

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.

ts
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
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
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
// 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
// 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
# 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
// 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.

Capture on the webhook ​

Your page never learns the payment result. When Maytes sends checkout.authorized, capture within 2 minutes, then show the order on your return_url page.

Go live ​

environmentOpensUse with
sandboxThe sandbox checkoutSandbox API credentials
productionThe live checkoutProduction 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 buttonUse when
Redirect to checkout_urlYou don't want JavaScript on the page

Something wrong with the button? Open an issue on the SDK repository, or email support@maytes.co.