---
url: /stripe-express-checkout.md
---
# 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.

::: tip 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](/stripe). You can do both.
:::

The Maytes Checkout Button sits in a full-width row of its own directly under Apple Pay, Google Pay and Link, matched to their shape with `renderButton(slot, { block: true, height: 48, radius: 6 })`.

**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](/checkout-button#create-the-checkout-on-your-server).

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

::: code-group

```html [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]
// 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 [React]
// 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 [Next.js]
'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 [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 [Angular]
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 [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.

::: details 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](/checkout-button#create-the-checkout-on-your-server).
:::

## Test and go live

* \[ ] Keep the button on `environment: 'sandbox'`, point your server at the [sandbox](/sandbox-environment), and pay with a [test card](/sandbox-environment#test-cards).
* \[ ] 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](/checkout-button#go-live).

## Troubleshooting

| What you see | What to do |
|---|---|
| Maytes is taller or rounder than the wallets | Match `height` to Stripe's `buttonHeight`, and `radius` to your Stripe appearance's `borderRadius` |
| Maytes floats between Stripe's rows, or sits beside them | Put Maytes in its own full-width row under Stripe's element, as in [step 1](#_1-make-room-in-the-express-row). 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 captured | Your webhook must capture within 2 minutes: see [Capture on the webhook](/checkout-button#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](/stripe-payment-records).

## Good to know

* **Stripe never sees a Maytes order:** no PaymentIntent, no Stripe fee, and nothing in your Stripe Dashboard unless you [record it](/stripe-payment-records). Money arrives through [Maytes settlements](/portal); refunds go through [Maytes refunds](/refunds).
* **Maytes returns no shipping address:** show the button where your page already knows the final total.
