Skip to content

Maytes for WooCommerce ​

Add Split with Maytes to your WooCommerce checkout with one WordPress plugin. Customers check out as usual and split the cost with their mates. Your theme, your Stripe setup and your card checkout stay exactly as they are.

Is this the right page?

Use it if your store runs WooCommerce, on the Checkout block or the classic checkout. On a checkout you built yourself, add the button by hand: see Maytes in your Stripe express row.

The WooCommerce Checkout block with Split with Maytes in a full-width row under Apple Pay, Google Pay and Link

The Checkout block with the plugin. The wallet buttons are placeholders: your store shows the ones Stripe offers each browser.

The plugin adds three things:

  • The Split with Maytes button, in a full-width row of its own directly under the express buttons, at the same size. It works on the Checkout block and on the classic checkout.
  • A checkout route on your site, POST /wp-json/maytes/v1/checkout, that turns the cart into a pending WooCommerce order and a Maytes checkout.
  • A webhook route on your site, POST /wp-json/maytes/v1/webhook, that captures the payment and marks the order paid, so your usual order emails go out.

Setup is four steps: install the plugin, add your credentials, subscribe the webhook, then test on sandbox. No custom code is needed.

How a payment flows ​

The order is paid only at step 7, when the capture succeeds. The order-received page at step 8 confirms the order; it doesn't trigger the payment.

Before you start ​

  • WordPress 6.5 or later, with WooCommerce 9.0 or later, on the Checkout block or the classic [woocommerce_checkout] checkout.
  • PHP 7.4 or later. The plugin has no other dependencies: nothing to install with Composer.
  • A public HTTPS site, so Maytes can deliver webhooks.
  • AUD as your store currency. The button stays hidden for other currencies.
  • Maytes merchant credentials for sandbox and production, from the merchant portal.

Express buttons such as Apple Pay, Google Pay and Link are optional. With them, Maytes sits in a row under them; without them, the Maytes row stands on its own at the top of the checkout.

SettingSandboxProduction
MAYTES_ENVsandboxproduction
MAYTES_API_ENDPOINThttps://sandbox-api.maytes.cohttps://api.maytes.co
MAYTES_CLIENT_ID / MAYTES_CLIENT_SECRETSandbox keys from the merchant portalProduction keys from the merchant portal
MAYTES_WEBHOOK_SECRETShown once, when the sandbox subscription is createdShown once, when the production subscription is created

Keep every setting on one environment

The button's environment comes from MAYTES_ENV, and the server calls MAYTES_API_ENDPOINT with your keys. All five settings must come from the same environment: a sandbox button with production keys fails.

1. Install the plugin ​

Coming to the WordPress.org plugin directory

Maytes for WooCommerce is waiting for review in the WordPress.org plugin directory. Until it's listed, install it from the zip below. Once it's listed, you can also find it under Plugins → Add New Plugin by searching for "Maytes", and get updates from there.

Download maytes-for-woocommerce.zip (version 1.0.0), then install it like any WordPress plugin:

text
Plugins → Add New Plugin → Upload Plugin
Choose maytes-for-woocommerce.zip → Install Now → Activate
bash
wp plugin install maytes-for-woocommerce.zip --activate
FileWhat it does
maytes-for-woocommerce.phpPlugin header, compatibility with the Checkout block and HPOS, loads the plugin
includes/class-maytes-woo.phpRenders the classic row, registers both REST routes, creates the checkout and captures on the webhook
includes/class-maytes-woo-blocks.phpRegisters Split with Maytes with the Checkout block
includes/class-maytes-woo-api.phpCalls the Maytes API with the WordPress HTTP API, with the OAuth token
includes/class-maytes-woo-webhook-verifier.phpVerifies the webhook signature, ported from the Maytes PHP SDK
assets/js/maytes-blocks.jsThe express payment method on the Checkout block
assets/js/maytes-classic.jsPlaces the row on the classic checkout and renders the button
assets/css/maytes.cssThe full-width row on both checkouts

Until step 2 is done, the button stays hidden and WordPress shows a notice on the Plugins screen.

2. Add your credentials ​

Add five constants to wp-config.php, above the line /* That's all, stop editing! */. They stay on your server: the plugin never stores them in the database or sends them to the browser. Start with sandbox values.

php
define( 'MAYTES_ENV',            'sandbox' );                       // 'production' when live
define( 'MAYTES_API_ENDPOINT',   'https://sandbox-api.maytes.co' ); // live: https://api.maytes.co
define( 'MAYTES_CLIENT_ID',      '…' );
define( 'MAYTES_CLIENT_SECRET',  '…' );
define( 'MAYTES_WEBHOOK_SECRET', '…' );                             // from step 3

3. Subscribe the webhook ​

In the merchant portal, subscribe your store's webhook route to the checkout.authorized and checkout.voided events:

text
https://shop.example.com/wp-json/maytes/v1/webhook

The portal shows the signing secret once, when the subscription is created. Copy it into MAYTES_WEBHOOK_SECRET. Then send a webhook.test event from the portal and check that the delivery log shows a 2xx. See Managing your subscription.

Plain permalinks

If Settings → Permalinks is set to Plain, /wp-json/ routes don't resolve. Pick any other structure, or subscribe https://shop.example.com/?rest_route=/maytes/v1/webhook instead.

4. Test, then go live ​

Run every check on sandbox before you switch anything to production. The test cards are on the sandbox page.

  • [ ] Pay with 4242 4242 4242 4242. The order moves to Processing with a "Paid with Split with Maytes" note, and your order emails go out.
  • [ ] Pay with the decline card 4000 0000 0000 0002. The order stays Pending payment and nothing is captured.
  • [ ] Click the button with the email empty. The checkout shows its usual error and no order is created.
  • [ ] Apply a coupon. The Maytes total matches the WooCommerce total to the cent.
  • [ ] Open two Maytes windows for one cart and pay in the first. The second checkout is cancelled if it authorizes.
  • [ ] Check the row at phone width, and with every express button your browsers offer. Maytes stays a full-width row under them.
  • [ ] Send webhook.test from the portal. The delivery log shows a 2xx.
  • [ ] Go live: set MAYTES_ENV to production and MAYTES_API_ENDPOINT to https://api.maytes.co, add your production keys, and subscribe the production webhook with its own secret.

How the plugin works ​

This section is for your developer. It explains what the plugin does on each request, so nothing here needs to be built.

The button ​

The button script loads from https://js.maytes.co/v1/checkout-button.js with crossorigin="anonymous", only on the checkout, and only when the cart is in AUD and needs payment. The button matches the express buttons: block: true fills the row, height: 48 and radius: 6. mode stays at its default, popup; on phones the button opens Maytes in the same tab.

On the Checkout block, Split with Maytes is an express payment method. WooCommerce lays express buttons out in a grid; while Maytes is there, the wallets keep sharing their row and Maytes takes a full-width row after them. If you set a button height or corner radius in the Checkout block's express payment settings, Maytes uses them too. Maytes appears on the checkout only, not on the Cart block, because it needs the customer's details.

On the classic checkout, the plugin prints an empty #maytes-express-row on the woocommerce_checkout_before_customer_details hook. The script moves it directly under Stripe's express element and renders the button, and moves it back each time WooCommerce refreshes the checkout (updated_checkout).

The classic WooCommerce checkout with Split with Maytes under the express buttons and above the "OR" separator

The classic checkout on the Storefront theme. The wallet buttons are placeholders for Stripe's express element.

Element on the classic checkoutWhat changes
#wc-stripe-express-checkout-element (Link, Apple Pay, Google Pay)Nothing. Stripe's iframe is never moved, because moving an iframe reloads it
#maytes-express-rowNew. Inserted directly after Stripe's element
#wc-stripe-express-checkout-button-separator ("— OR —")Nothing. It now sits under Maytes
Billing details, order review, card paymentNothing

The button checks the customer's details first. Maytes doesn't return a name, email or address, and your orders need them for the confirmation email. If a required field is empty, the email is invalid or the terms box is unticked, the customer sees the checkout's usual error and no order is created: inline field errors on the Checkout block, WooCommerce's error list on the classic checkout. Wallets like Link skip this check because they supply the details.

On click, the button posts the customer's details to /wp-json/maytes/v1/checkout with a WordPress REST nonce and a plugin nonce. It maps the reply to { checkoutId: checkout_uuid, checkoutUrl: checkout_url }, and opens that URL. On the Checkout block, closing the Maytes window hands the checkout back, with the customer's previous payment method selected.

Creating the checkout ​

POST /wp-json/maytes/v1/checkout builds the order with WooCommerce's own WC_Checkout::create_order(), so coupons, GST, shipping, booking dates and any other order data come out exactly as they would for a card payment.

  1. Load the cart. Return 400 if it's empty.
  2. Check the details: on the classic checkout, every required billing (and shipping) field, the email format and the terms box; on the Checkout block, which validates its own fields in the browser, the email. Return 422 with the messages if any fail.
  3. Create the pending order, or reuse the one in the session's order_awaiting_payment, so a second click on the same cart doesn't create a second order.
  4. For a new order, fire woocommerce_checkout_order_processed, so booking and stock plugins react as they do for a card checkout.
  5. Create the Maytes checkout.
  6. Save checkout_uuid on the order as _maytes_checkout_uuid. Every uuid is kept, in case the customer opens two windows.
  7. Return checkout_uuid and checkout_url exactly as Maytes sent them.
php
$created = self::api()->create_checkout( array(
	'merchant_order_id' => (string) $order->get_id(),
	'total_amount'      => self::minor( $order->get_total() ), // cents
	'currency'          => $order->get_currency(),
	'return_url'        => $order->get_checkout_order_received_url(),
	'cancel_url'        => wc_get_checkout_url(),
	'items'             => $items, // category 'other' line items
) );

The plugin calls the Maytes API with the WordPress HTTP API, so it needs no Composer packages. It gets an OAuth token from /oauth/token with your client credentials, and fetches a new one once if a call returns 401. If Maytes returns 409 order_already_authorized, the customer is told the order is already being paid with Maytes.

How amounts are sent ​

Maytes rejects a checkout with 422 invalid_amount_arithmetic unless the line items add up to total_amount (Create a checkout). Coupons reduce each line's total, and prices that include GST carry their GST, so every line goes out at its final price in cents. For example, a $65 class with a $13 coupon:

WooCommerce lineAmountSent to Maytes
Watercolour class, 1 seat$65.00One item, unit_price 5200
Coupon SPRING20−$13.00Included in the item price
GST (included)$4.73Included in the item price
Order total$52.00total_amount 5200

Fees and shipping go out as their own items. If your store prices exclude tax, the tax goes out as its own item too. If rounding leaves the lines a cent away from the total, or a fee is negative, the plugin sends a single "<Your store> order #…" line for the exact total instead.

Confirming the payment ​

An order is paid only when Maytes sends checkout.authorized and the plugin's capture succeeds. Capture must happen within 2 minutes, so the plugin captures straight away and leaves the emails to WooCommerce.

php
$event = Maytes_Woo_Webhook_Verifier::verify(
	$request->get_body(),                       // the raw bytes, never re-encoded JSON
	$request->get_header( 'x-maytes-signature' ),
	MAYTES_WEBHOOK_SECRET
); // throws Maytes_Woo_Webhook_Error: reply 400

self::api()->capture_checkout( $uuid, array(
	'expected_total_amount' => self::minor( $order->get_total() ),
	'currency'              => $order->get_currency(),
	'merchant_order_id'     => (string) $order->get_id(),
) );

$order->payment_complete( $uuid ); // your usual order emails go out

Each step leaves a note on the order, newest first:

WooCommerce order notes: Maytes checkout created, Payment via Split with Maytes, and Paid with Split with Maytes, each with the checkout id

The verifier is ported from the Maytes PHP SDK: it checks the signature over the raw body, accepts either signature during a secret rotation, compares in constant time and rejects deliveries more than 5 minutes old. The order is found by merchant_order_id, and the event's checkout_uuid must be one saved on that order. Processed event ids are kept for 4 days, because deliveries are at-least-once.

SituationWhat the plugin doesReply
Bad or stale signatureLogs the reason and stops400
Event id already handledNothing200
Unknown checkout, order already paid, or order no longer payableCancels the checkout, to release the customer's funds200
Your maytes_woo_can_fulfil filter returns falseCancels the checkout, and the order is marked Failed200
Capture succeedspayment_complete(), and an order note200
Capture returns 409 or 422The order is marked Failed200
Capture hits a network error or a 5xxNothing yet: Maytes retries inside the 2-minute window503
checkout.voided on an unpaid orderThe order is marked Failed, with the reason200

Order statuses ​

A Maytes order uses WooCommerce's usual statuses, and Split with Maytes shows as its payment method.

An order moves to Failed when its authorization is voided or expires, when the capture is refused, or when your filter releases it. A declined card doesn't change the order: the customer can try again in the Maytes window, and the order stays Pending payment. Like any unpaid WooCommerce order, it's cancelled after your Hold stock time when stock management is on.

Last-minute checks ​

Use the maytes_woo_can_fulfil filter to release a payment before any money is taken, for example when a class has just filled up. It runs inside the webhook, after the authorization and before the capture, so keep it fast: deliveries time out after 15 seconds.

php
add_filter(
	'maytes_woo_can_fulfil',
	function ( bool $can_fulfil, WC_Order $order ): bool {
		return $can_fulfil && my_store_has_seats_for( $order );
	},
	10,
	2
);

Returning false cancels the Maytes checkout, so the customer's funds are released and nothing is captured. Run longer checks before the customer reaches the checkout.

Troubleshooting ​

The plugin logs to WooCommerce → Status → Logs, under the source maytes.

What you seeWhat to do
No Maytes button, and a "needs your Maytes settings" noticeAdd all five constants to wp-config.php, with MAYTES_ENV set to sandbox or production
No Maytes button, and no noticeCheck that the store currency is AUD and that the cart has something to pay. Maytes shows on the checkout, not on the cart
Maytes sits at the top of the classic checkout, not under the walletsStripe's express buttons aren't on the page. Turn on express checkout in the WooCommerce Stripe Gateway settings
"We couldn't start Split with Maytes"Check the maytes log. Usually the keys don't belong to the environment in MAYTES_API_ENDPOINT
The delivery log shows 404Your permalinks are set to Plain: see step 3
The delivery log shows 400The signing secret doesn't match this subscription. Each environment has its own secret
The delivery log shows 401 or 403A security plugin or firewall is blocking the REST API. Allow POST /wp-json/maytes/v1/webhook
The order stays Pending after a successful paymentCheck that the subscription includes checkout.authorized and that the delivery log shows a 2xx

Good to know ​

  • Maytes payments stay outside Stripe. They don't appear in your Stripe Dashboard, the money arrives through Maytes settlements, and refunds are made in the Maytes portal, not from the WooCommerce order screen. To show them in Stripe as well, see Record payments in Stripe.
  • Nothing to turn on under WooCommerce → Settings → Payments. The plugin adds its own button, so Maytes isn't listed as a gateway there.
  • Customer details: Maytes returns no name, email or address, so the button checks your checkout form first.
  • Questions: email support@maytes.co.

Related: Maytes Checkout Button · Maytes in your Stripe express row · Create a checkout · Webhooks · Capture & checkout status · Sandbox environment