Skip to content

Add Maytes to your Stripe express row ​

Put Split with Maytes in your express row, in a full-width row of its own under Apple Pay, Google Pay and Link. It's our button: nothing changes in your Stripe account or your Stripe payment code.

Is this the right page?

Use it to put Maytes under Stripe's wallet buttons. To offer Maytes as a payment method inside Stripe's payment form, see Maytes in your Stripe Payment Element. You can do both.

You add two things: a slot under Stripe's Express Checkout Element, and the Maytes button in it. Your server needs the standard Maytes endpoint and webhook from the checkout button guide.

1. Make room in the express row ​

Stripe draws its wallets inside its own iframe, so Maytes goes under the Express Checkout Element, not inside it. Stripe's wallet row changes height with the wallets a browser offers, so give Maytes a row of its own instead of a cell beside it:

html
<div class="express-row">
  <div id="stripe-express"></div>
  <div id="maytes-button"></div>
</div>
css
.express-row {
  display: grid;
  gap: 8px;
}

Mount Stripe's element in the first row, with its buttons 48px tall. Leave Stripe's layout at its default, because the number of wallets and rows changes from browser to browser:

js
const express = elements.create('expressCheckout', { buttonHeight: 48 });
express.mount('#stripe-express');

2. Add the Maytes button ​

Render the button in the second row. Three options match it to the wallets: block fills the row, height: 48 matches Stripe's buttonHeight, and radius: 6 matches Stripe's default corners.

html
<script
  src="https://js.maytes.co/v1/checkout-button.js"
  crossorigin="anonymous"
></script>
<script>
  const maytes = window.Maytes({
    environment: 'sandbox',
    createCheckout: () => createMaytesCheckout('cart_8fd21'),
  });
  maytes.renderButton(document.getElementById('maytes-button'), {
    block: true,
    height: 48,
    radius: 6,
  });
</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, height: 48, radius: 6 });
tsx
// npm install @maytes/checkout-button
import { ExpressCheckoutElement } from '@stripe/react-stripe-js';
import {
  MaytesProvider,
  MaytesButton,
} from '@maytes/checkout-button/react';

// Render inside your <Elements> provider.
export function ExpressRow({ cartId }: { cartId: string }) {
  return (
    <div className="express-row">
      <ExpressCheckoutElement
        options={{ buttonHeight: 48 }}
        onConfirm={confirmWithStripe}
      />
      <MaytesProvider
        environment="sandbox"
        createCheckout={() => createMaytesCheckout(cartId)}
      >
        <MaytesButton block height={48} radius={6} />
      </MaytesProvider>
    </div>
  );
}
tsx
'use client'; // Stripe and Maytes both run in the browser
import { ExpressCheckoutElement } from '@stripe/react-stripe-js';
import {
  MaytesProvider,
  MaytesButton,
} from '@maytes/checkout-button/react';

// Render inside your <Elements> provider.
export function ExpressRow({ cartId }: { cartId: string }) {
  return (
    <div className="express-row">
      <ExpressCheckoutElement
        options={{ buttonHeight: 48 }}
        onConfirm={confirmWithStripe}
      />
      <MaytesProvider
        environment="sandbox"
        createCheckout={() => createMaytesCheckout(cartId)}
      >
        <MaytesButton block height={48} radius={6} />
      </MaytesProvider>
    </div>
  );
}
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,
    height: 48,
    radius: 6,
  });
});
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: 'maytes-cell',
  standalone: true,
  template: '<div #slot></div>',
})
export class MaytesCellComponent 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,
      height: 48,
      radius: 6,
    });
  }

  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, height: 48, radius: 6 });
    return () => maytes.destroy();
  });
</script>

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

In Vue, Angular and Svelte, the component above is the Maytes row: place it directly under Stripe's element.

Where createMaytesCheckout comes from

It's a small function on your page that asks your server for the checkout:

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. It calls Maytes's POST /api/merchant/v1/checkouts with your credentials: see Create the checkout on your server.

Test and go live ​

  • [ ] Keep the button on environment: 'sandbox', point your server at the sandbox, and pay with a test card.
  • [ ] Check the row at phone width and with every wallet your browsers offer, such as Apple Pay in Safari and Google Pay in Chrome. Maytes stays full width under them.
  • [ ] Switch to environment: 'production' with production credentials, then follow the go-live checklist.

Troubleshooting ​

What you seeWhat to do
Maytes is taller or rounder than the walletsMatch height to Stripe's buttonHeight, and radius to your Stripe appearance's borderRadius
Maytes floats between Stripe's rows, or sits beside themPut Maytes in its own full-width row under Stripe's element, as in step 1. Stripe's wallet row changes height with the wallets a browser offers, so a cell beside it can misalign
The order is authorized but never capturedYour webhook must capture within 2 minutes: see Capture on the webhook

Record Maytes payments in Stripe ​

A Maytes order doesn't appear in your Stripe Dashboard on its own. To show it there with a Stripe receipt, see Record Maytes payments in Stripe.

Good to know ​

  • Stripe never sees a Maytes order: no PaymentIntent, no Stripe fee, and nothing in your Stripe Dashboard unless you record it. Money arrives through Maytes settlements; refunds go through Maytes refunds.
  • Maytes returns no shipping address: show the button where your page already knows the final total.