Skip to content

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.

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: it's less work, with nothing to set up in Stripe. You can do both.

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.

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:

    The Split with Maytes logo

    Download the logo (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

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.

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.

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 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
// 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
'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
<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
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
<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. block makes the Maytes button full width; add height and radius to match your Pay button (Button options). Don't call stripe.confirmPayment() for Maytes: there's no PaymentIntent to confirm.

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 ​

  • [ ] 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, 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.

Troubleshooting ​

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