Appearance
Maytes Checkout Button — integration guide
Add a Split with Maytes button to your checkout. When a shopper clicks it, your createCheckout callback fetches a Maytes checkout ID from your backend, and the button opens the Maytes-hosted checkout in a popup while your page sits behind a loading overlay. When the popup closes, the overlay clears.
For everything to do with creating and capturing checkouts on the server, see Create a checkout and Capture & order status. This guide is the frontend integration only.
1. Add the script
html
<script
src="https://js.maytes.co/v1/checkout-button.js"
crossorigin="anonymous"
></script>The script exposes a single global: window.Maytes.
2. Configure and render
window.Maytes is a factory function. Call it once with two things and it returns the button SDK:
createCheckout— anasyncfunction called on click. It must call your backend (which holds your OAuth credentials and creates the checkout) and resolve to{ checkoutId: '<the checkout_uuid>' }.environment—'sandbox'for the sandbox environment,'production'for live. The button derives the Maytes-hosted checkout URL from this — you never passcheckout_url.
Then mount it with renderButton:
html
<div id="maytes-slot"></div>js
const maytes = window.Maytes({
createCheckout: async () => {
const { checkout_uuid } = await fetch('/api/storefront/create-maytes-session', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ cart }),
}).then((r) => r.json());
return { checkoutId: checkout_uuid };
},
environment: 'sandbox',
});
const removeButton = maytes.renderButton(
document.getElementById('maytes-slot'),
{ block: true },
);createCheckout must resolve to { checkoutId: 'the-uuid' }. If it rejects or returns the wrong shape, the button re-enables, the overlay clears, and the error is logged to the console — no popup opens.
renderButton returns a cleanup function — call removeButton() when navigating away (e.g. in a React useEffect cleanup).
Options
| Option | Default | Effect |
|---|---|---|
label | 'Split with' | Text shown before the Maytes logo. |
block | false | When true, full-width instead of inline pill. |
Why create on click, not ahead of time
The checkout reflects the cart at the moment the shopper decides to pay. Minting up front leads to stale carts and abandoned sessions.
3. What happens on click
- The button shows a spinner and disables.
- A full-screen overlay appears on your page.
- Your
createCheckout()runs and resolves with{ checkoutId }. - The Maytes-hosted checkout opens in a centered popup; the overlay stays on your page while the shopper pays.
- The shopper authorizes. In the webhook flow, your backend receives
checkout.authorizedand captures while the shopper sees a processing screen inside the popup — then the shopper continues in Maytes (including the split step, if enabled) and the popup redirects itself to yourreturn_url. In the legacy flow, the popup redirects toreturn_urlright after authorization. - The shopper closes the popup (or it closes on completion) → the overlay clears and the button re-enables.
Popup blocked? If the browser blocks window.open (rare — it's only called inside a user click), the button falls back to a same-window redirect so the flow doesn't die, and logs a console.warn.
4. After the popup closes
By design, your merchant page does not receive a structured JavaScript callback with the payment result — the shopper may close the popup early, or their network may drop mid-flight, so any in-page callback would be unreliable.
Where your truth comes from instead:
- Webhook flow: the
checkout.authorizedwebhook → your capture → your order record. The page the shopper eventually lands on (return_url) just reads that record. - Redirect flow (legacy): the shopper's landing on
return_url/cancel_urlis the signal you build on.
This is the same model as other hosted checkout products: the browser is the untrusted channel; your backend is the source of truth.
5. Environments
environment | Maytes checkout popup |
|---|---|
sandbox | The sandbox environment checkout — pairs with your sandbox API credentials. |
production | The live checkout — pairs with your production credentials. |
You never set the URL yourself — pick the environment and the button does the rest. Make sure the environment matches the credentials your backend uses to create the checkout: a checkout created against the sandbox API can only be opened by the sandbox button.
6. Framework examples
The button is framework-agnostic — configure with the factory, mount with renderButton, and use the returned cleanup function. Below are idiomatic patterns for the three frameworks merchants ask about most.
html
<!-- Maytes Checkout Button + jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://js.maytes.co/v1/checkout-button.js" crossorigin="anonymous"></script>
<div id="maytes-slot"></div>
<script>
$(function () {
// 1. Configure the Maytes Checkout Button once.
const maytes = window.Maytes({
createCheckout: async () => {
// jQuery's $.ajax returns a thenable — await it.
const data = await $.ajax({
url: '/api/storefront/create-maytes-session',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ cart: window.cart }),
});
return { checkoutId: data.checkout_uuid };
},
environment: 'sandbox',
});
// 2. Render the button into the slot.
maytes.renderButton(document.getElementById('maytes-slot'), { block: true });
});
</script>tsx
// MaytesButton.tsx
import { useEffect, useRef } from 'react';
type Cart = { items: Array<{ ref: string; qty: number; price: number }> };
export function MaytesButton({ cart }: { cart: Cart }) {
const slotRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (slotRef.current === null || typeof window.Maytes !== 'function') return;
// 1. Configure the button. Re-runs if `cart` changes so
// `createCheckout` always closes over the latest cart.
const maytes = window.Maytes({
createCheckout: async () => {
const res = await fetch('/api/storefront/create-maytes-session', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ cart }),
});
const { checkout_uuid } = await res.json();
return { checkoutId: checkout_uuid };
},
environment: 'sandbox',
});
// 2. Render into our ref. The returned cleanup fn removes the
// button when the component unmounts.
const removeButton = maytes.renderButton(slotRef.current, { block: true });
return () => removeButton();
}, [cart]);
return <div ref={slotRef} />;
}ts
// maytes-button.component.ts
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import { CartService } from './cart.service';
declare global {
interface Window {
Maytes?: (opts: {
createCheckout: () => Promise<{ checkoutId: string }>;
environment: 'sandbox' | 'production';
}) => { renderButton: (el: HTMLElement, opts?: { block?: boolean }) => () => void };
}
}
@Component({
selector: 'app-maytes-button',
standalone: true,
template: '<div #slot></div>',
})
export class MaytesButtonComponent implements OnInit, OnDestroy {
@ViewChild('slot', { static: true }) slot!: ElementRef<HTMLDivElement>;
private removeButton?: () => void;
private cart = inject(CartService);
ngOnInit(): void {
if (typeof window.Maytes !== 'function') return;
// 1. Configure the Maytes Checkout Button.
const maytes = window.Maytes({
createCheckout: async () => {
const res = await fetch('/api/storefront/create-maytes-session', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ cart: this.cart.snapshot() }),
});
const { checkout_uuid } = await res.json();
return { checkoutId: checkout_uuid };
},
environment: 'sandbox',
});
// 2. Render into the @ViewChild slot.
this.removeButton = maytes.renderButton(this.slot.nativeElement, { block: true });
}
ngOnDestroy(): void {
this.removeButton?.();
}
}Load the script before your code runs (a plain <script> tag in the document head, or a framework loader with an onload hook) and feature-detect typeof window.Maytes === 'function' before calling it.
FAQ
Can my page's JavaScript know the payment result? No — see After the popup closes. Build your post-checkout UX on your backend's order state (webhook flow) or the return_url / cancel_url landing (legacy flow).
Does the button work outside React / Angular / jQuery? Yes — it's framework-agnostic. The pattern is always the same: call the window.Maytes factory with your createCheckout and environment, then renderButton to mount the UI. Vue, Svelte, Solid, plain JS, server-rendered apps — all work.
Multiple buttons on one page? Render into multiple slots; each renderButton call returns its own cleanup function. One checkout runs at a time per page.
