---
url: /stripe.md
---
# Add Maytes to your Stripe Payment Element

Show **Split with Maytes** as a payment method inside Stripe's Payment Element. When the customer picks it, the Maytes button takes the place of your Pay button and opens the Maytes checkout. Stripe takes no payment for that order.

::: tip Is this the right page?
Use it to offer Maytes as a payment method inside Stripe's payment form. To put Maytes under Apple Pay and Google Pay instead, see [Maytes in your Stripe express row](/stripe-express-checkout): it's less work, with nothing to set up in Stripe. You can do both.
:::

Split with Maytes appears as a payment method in the Payment Element. While it is selected, the Maytes Checkout Button is shown in place of the merchant's Pay button and opens the Maytes checkout.

**You add three things:** Split with Maytes in your Stripe Dashboard, one entry in your Elements options, and the Maytes button where your Pay button goes. Your server needs the standard Maytes endpoint and webhook from the [checkout button guide](/checkout-button#create-the-checkout-on-your-server).

## 1. Create Split with Maytes in Stripe

Stripe only shows a custom payment method to the account that created it. Maytes cannot create it for you, so you add it to your own Stripe account once per environment.

1. In your Stripe Dashboard, open **Settings → Payments → Custom payment methods** and click **Create**.

2. Name it exactly **`Split with Maytes`**.

3. Upload this logo:

   [Download the logo](/stripe/split-with-maytes-logo.png){download} (PNG, 1200 × 375).

4. Copy the `cpmt_…` ID Stripe shows next to it. That is your `MAYTES_CPM_ID` for step 2.

![Split with Maytes listed under Custom payment methods, with its cpmt\_ ID](/stripe/custom-payment-method-created.png)

Do this in test mode and again in live mode. Each mode gets its own ID, so keep each `cpmt_…` beside the publishable key from the same mode.

::: warning A mismatched key and ID fail silently
If the `cpmt_` ID comes from a different Stripe account or mode than your publishable key, Stripe drops it: the Payment Element just shows no Split with Maytes. The only sign is a browser console warning, `The following custom payment method types are not enabled`.
:::

## 2. Add it to your Payment Element

Add your `cpmt_` ID to `customPaymentMethods`. Elements must be in deferred mode: `mode`, `amount` and `currency`, with no client secret.

```js
const elements = stripe.elements({
  mode: 'payment',
  amount: 4500, // your cart total in minor units: AUD 45.00
  currency: 'aud',
  customPaymentMethods: [
    {
      id: MAYTES_CPM_ID, // your cpmt_ ID for this Stripe mode
      payment: {
        type: 'static',
        subtitle: 'Split the cost with friends',
      },
    },
  ],
});
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
```

Stripe lists custom payment methods last. To show Split with Maytes first, pass `paymentMethodOrder: [MAYTES_CPM_ID, 'card']` to `elements.create('payment', …)`.

In React, pass the same options to `<Elements options={…}>`. In TypeScript, cast the Maytes entry once: Stripe reads `payment`, but `@stripe/stripe-js` 10 still types the field as `options`.

```ts
import type { CustomPaymentMethod } from '@stripe/stripe-js';

const maytesMethod = {
  id: MAYTES_CPM_ID,
  payment: { type: 'static', subtitle: 'Split the cost with friends' },
} as unknown as CustomPaymentMethod;
```

## 3. Add the Maytes button

Show the Maytes button in place of your Pay button while Split with Maytes is selected. The Payment Element's `change` event tells you which method is selected.

Your Pay button is the one you already have for cards, shown here calling `payWithStripe()`; it doesn't change. The only Maytes part is the Maytes button, which opens the checkout itself.

::: code-group

```html [HTML]
<button id="pay" type="button">Pay</button>
<div id="maytes-button" hidden></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 payButton = document.getElementById('pay');
  const maytesSlot = document.getElementById('maytes-button');
  maytes.renderButton(maytesSlot, { block: true });
  payButton.addEventListener('click', payWithStripe);

  paymentElement.on('change', (event) => {
    const maytesSelected = event.value.type === MAYTES_CPM_ID;
    payButton.hidden = maytesSelected;
    maytesSlot.hidden = !maytesSelected;
  });
</script>
```

```ts [npm]
// npm install @maytes/checkout-button
import { Maytes } from '@maytes/checkout-button';

const maytes = Maytes({
  environment: 'sandbox',
  createCheckout: () => createMaytesCheckout('cart_8fd21'),
});
const payButton = document.getElementById('pay')!;
const maytesSlot = document.getElementById('maytes-button')!;
maytes.renderButton(maytesSlot, { block: true });
payButton.addEventListener('click', payWithStripe);

paymentElement.on('change', (event) => {
  const maytesSelected = event.value.type === MAYTES_CPM_ID;
  payButton.hidden = maytesSelected;
  maytesSlot.hidden = !maytesSelected;
});
```

```tsx [React]
// npm install @maytes/checkout-button
import { useState } from 'react';
import { PaymentElement } from '@stripe/react-stripe-js';
import {
  MaytesProvider,
  MaytesButton,
} from '@maytes/checkout-button/react';

// Render inside your <Elements> provider from step 2.
export function Payment({ cartId }: { cartId: string }) {
  const [method, setMethod] = useState('');

  return (
    <MaytesProvider
      environment="sandbox"
      createCheckout={() => createMaytesCheckout(cartId)}
    >
      <PaymentElement onChange={(event) => setMethod(event.value.type)} />
      {method === MAYTES_CPM_ID ? (
        <MaytesButton block />
      ) : (
        <button type="button" onClick={payWithStripe}>Pay</button>
      )}
    </MaytesProvider>
  );
}
```

```tsx [Next.js]
'use client'; // Stripe and Maytes both run in the browser
import { useState } from 'react';
import { PaymentElement } from '@stripe/react-stripe-js';
import {
  MaytesProvider,
  MaytesButton,
} from '@maytes/checkout-button/react';

// Render inside your <Elements> provider from step 2.
export function Payment({ cartId }: { cartId: string }) {
  const [method, setMethod] = useState('');

  return (
    <MaytesProvider
      environment="sandbox"
      createCheckout={() => createMaytesCheckout(cartId)}
    >
      <PaymentElement onChange={(event) => setMethod(event.value.type)} />
      {method === MAYTES_CPM_ID ? (
        <MaytesButton block />
      ) : (
        <button type="button" onClick={payWithStripe}>Pay</button>
      )}
    </MaytesProvider>
  );
}
```

```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>();
const maytesSelected = ref(false);
let maytes: MaytesSDK | undefined;

onMounted(() => {
  maytes = Maytes({
    environment: 'sandbox',
    createCheckout: () => createMaytesCheckout(props.cartId),
  });
  maytes.renderButton(slot.value!, { block: true });
  paymentElement.on('change', (event) => {
    maytesSelected.value = event.value.type === MAYTES_CPM_ID;
  });
});
onBeforeUnmount(() => maytes?.destroy());

function pay() {
  payWithStripe();
}
</script>

<template>
  <button v-show="!maytesSelected" type="button" @click="pay">Pay</button>
  <div v-show="maytesSelected" ref="slot" />
</template>
```

```ts [Angular]
import {
  AfterViewInit,
  Component,
  ElementRef,
  Input,
  OnDestroy,
  ViewChild,
  signal,
} from '@angular/core';
import { Maytes, type MaytesSDK } from '@maytes/checkout-button';

@Component({
  selector: 'pay-buttons',
  standalone: true,
  template: `
    <button type="button" [hidden]="maytesSelected()" (click)="pay()">
      Pay
    </button>
    <div #slot [hidden]="!maytesSelected()"></div>
  `,
})
export class PayButtonsComponent implements AfterViewInit, OnDestroy {
  @Input() cartId = '';
  @ViewChild('slot') slot!: ElementRef<HTMLDivElement>;
  protected readonly maytesSelected = signal(false);
  private maytes?: MaytesSDK;

  ngAfterViewInit() {
    this.maytes = Maytes({
      environment: 'sandbox',
      createCheckout: () => createMaytesCheckout(this.cartId),
    });
    this.maytes.renderButton(this.slot.nativeElement, { block: true });
    paymentElement.on('change', (event) => {
      this.maytesSelected.set(event.value.type === MAYTES_CPM_ID);
    });
  }

  pay() {
    payWithStripe();
  }

  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;
  let maytesSelected = $state(false);

  // 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 });
    paymentElement.on('change', (event) => {
      maytesSelected = event.value.type === MAYTES_CPM_ID;
    });
    return () => maytes.destroy();
  });
</script>

<button type="button" hidden={maytesSelected} onclick={payWithStripe}>
  Pay
</button>
<div bind:this={slot} hidden={!maytesSelected}></div>
```

:::

`paymentElement` is the element from [step 2](#_2-add-it-to-your-payment-element). `block` makes the Maytes button full width; add `height` and `radius` to match your Pay button ([Button options](/checkout-button#button-options)). Don't call `stripe.confirmPayment()` for Maytes: there's no PaymentIntent to confirm.

::: 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

* \[ ] With your `pk_test_` key and your test `cpmt_` ID, check that picking Split with Maytes shows the Maytes button, and that it opens Maytes.
* \[ ] Pay one order with Maytes, using a [sandbox test card](/sandbox-environment#test-cards), and one with a card through Stripe.
* \[ ] Go live with your `pk_live_` key and your live-mode `cpmt_` ID, then follow the [go-live checklist](/checkout-button#go-live).

## Troubleshooting

| What you see | What to do |
|---|---|
| Split with Maytes doesn't appear, console says `custom payment method types are not enabled` | Use the `cpmt_` ID from your own Stripe account, in the same environment as your publishable key |
| Elements complains about a missing client secret | Use deferred mode: `mode`, `amount` and `currency`, with no client secret |
| Your Pay button still shows beside the Maytes button | Your CSS sets `display` on it, which overrides `hidden`: add `[hidden] { display: none !important; }` |
| 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 doesn't process a Maytes order:** there's no PaymentIntent. Money arrives through [Maytes settlements](/portal); refunds go through [Maytes refunds](/refunds), never Stripe.
* **Send a cart reference, not a total:** your server prices the cart from your own catalogue. See [Amount rules](/create-checkout#amount-rules).
* **Keep Stripe and Maytes webhooks on separate routes:** they use different signing secrets.
