Appearance
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 skillTry it
How it works
- 1Customer clicks Split with MaytesYour page
- 2Your server creates the checkoutYour server
- 3Customer pays their shareMaytes
- 4Your 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.
| Placement | Use it when | Guide |
|---|---|---|
| On its own | Maytes is a pay button of its own on your page | 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 |
| 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 |
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.
| 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.
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_jsoncsharp
// 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
environment | Opens | Use with |
|---|---|---|
sandbox | The sandbox checkout | Sandbox API credentials |
production | The live checkout | Production credentials |
- Set the button to
environment: 'production'. - Point your server at your production credentials.
- 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 | You don't want JavaScript on the page |
Something wrong with the button? Open an issue on the SDK repository, or email support@maytes.co.
