Skip to content

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 — an async function 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 pass checkout_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

OptionDefaultEffect
label'Split with'Text shown before the Maytes logo.
blockfalseWhen 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

  1. The button shows a spinner and disables.
  2. A full-screen overlay appears on your page.
  3. Your createCheckout() runs and resolves with { checkoutId }.
  4. The Maytes-hosted checkout opens in a centered popup; the overlay stays on your page while the shopper pays.
  5. The shopper authorizes. In the webhook flow, your backend receives checkout.authorized and 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 your return_url. In the legacy flow, the popup redirects to return_url right after authorization.
  6. 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.authorized webhook → 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_url is 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

environmentMaytes checkout popup
sandboxThe sandbox environment checkout — pairs with your sandbox API credentials.
productionThe 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.