Skip to content

Quickstart ​

Use the webhook flow to create a captured payment in about 30 minutes.

You will get a token, create a checkout, configure a capture webhook, and complete a payment. The guide uses raw curl so each API call is visible.

This is a proof of concept, not a production integration

This guide deliberately omits requirements for production, including signature verification, deduplication, and error handling. Relevant steps flag these omissions. Build your integration shows how to implement them with the backend SDK.

Run the guide in the sandbox environment so no real money moves. The examples show the production base URL; replace it with the sandbox base URL and use sandbox credentials.

Prerequisites ​

  • A Maytes merchant account with merchant portal access. See Onboarding if needed.
  • Your client_id and client_secret, from the portal (use the sandbox pair while testing).
  • A server that can make HTTPS requests. This guide uses curl; production integrations should use the backend SDK.
  • From Step 3 onward, a publicly reachable HTTPS endpoint for webhooks. For local development, place a tunnel such as ngrok or cloudflared in front of your development server.

The base URL throughout this guide:

https://api.maytes.co

For testing, replace this with the base URL from Sandbox environment.

Step 1 — Get an access token ​

Exchange your OAuth 2.0 client credentials for a short-lived access token.

bash
curl -s https://api.maytes.co/oauth/token \
  -H 'content-type: application/json' \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'

Export the returned access_token. Every later call sends it as a Bearer token:

bash
export ACCESS_TOKEN='paste_the_access_token_here'

See Authentication for caching and refresh.

Use the SDK

The backend SDKs fetch, cache, and refresh tokens. You provide client_id and client_secret once. This quickstart continues with raw HTTP to show each call.

Step 2 — Create a checkout ​

Create a checkout with the order's line items and total. return_url and cancel_url receive the customer after completion or abandonment. In the webhook flow, return_url is a confirmation page, not the fulfillment trigger. Capture happens in your webhook handler in Step 5.

bash
curl -s https://api.maytes.co/api/merchant/v1/checkouts \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "merchant_order_id": "order_1001",
    "total_amount": 4500,
    "currency": "AUD",
    "return_url": "https://shop.example.com/thanks?order=order_1001",
    "cancel_url": "https://shop.example.com/cart",
    "items": [
      {
        "item_ref": "GA-1",
        "name": "General Admission",
        "quantity": 1,
        "unit_price": 4500,
        "currency": "AUD",
        "category": "ticket",
        "custom_data": {
          "event_id": "evt_123",
          "event_start_at": "2026-12-05T19:00:00Z",
          "timezone": "Australia/Sydney"
        }
      }
    ]
  }'

Response:

json
{
  "data": {
    "checkout_uuid": "019fbb9e-ecd1-7059-99f3-f8bf47b63931",
    "status": "created",
    "checkout_url": "https://checkout.maytes.co/?id=019fbb9e-ecd1-7059-99f3-f8bf47b63931",
    "expires_at": null
  }
}

Keep these two response fields:

  • checkout_uuid — the order's identifier in Maytes. Webhooks, capture, status lookups, and support tooling use it. In production, store it with your order record. For this guide, export it:

    bash
    export CHECKOUT_UUID='019fbb9e-ecd1-7059-99f3-f8bf47b63931'
  • checkout_url — the Maytes-hosted payment page used in Step 3.

An unpaid checkout is kept for one hour after creation (expires_at is null unless you sent merchant_expires_at). You may exceed this while configuring webhooks, so keep the create command available. Step 6 uses it to create a new payable checkout.

See Create a checkout for fees, discounts, tax, item categories, and amount rules.

Step 3 — Send the customer to the Maytes window ​

Send the customer to the checkout_url returned in Step 2. It points to the Maytes-hosted checkout and includes the checkout_uuid as the id query parameter:

https://checkout.maytes.co/?id=019fbb9e-ecd1-7059-99f3-f8bf47b63931

Use the returned URL; do not construct it. Each environment has a different host. Hardcoding the URL can send sandbox customers to production.

Open the URL with the Maytes Checkout Button, which opens it in a popup on wide screens and in the same tab on phones, or redirect the browser yourself:

js
res.redirect(checkout.data.checkout_url);

Maytes handles the following steps in the window:

  1. The customer signs in with their mobile number and the code sent by SMS.
  2. The customer chooses how to split the cost between their payment and their mates.
  3. The customer pays by card. The funds are now authorized: held, not taken.
  4. Maytes sends checkout.authorized to your backend. Steps 4–5 handle this event.

Complete these steps in Step 6, after your capture handler exists. Otherwise, the uncaptured authorization is auto-voided after 2 minutes.

Step 4 — Subscribe your webhook endpoint ​

Register your endpoint for checkout.authorized and checkout.voided. The first event tells your backend that a payment is ready to capture.

Option A — in the merchant portal ​

In the merchant portal, open webhook configuration and set:

  • Endpoint URL — the public HTTPS URL that receives POST requests, for example https://shop.example.com/webhooks/maytes. For local development, use your ngrok or cloudflared tunnel.
  • Event types — tick checkout.authorized and checkout.voided.

Save the configuration. The portal shows your signing secret and also provides the delivery log and webhook.test button. Portal access is set up during onboarding.

Option B — via the API ​

To script the same setup:

bash
curl -s https://api.maytes.co/api/merchant/v1/webhooks/subscription \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://shop.example.com/webhooks/maytes",
    "event_types": ["checkout.authorized", "checkout.voided"]
  }'

Whichever option you use, the signing_secret is shown once — store it now. This guide does not use it, but production handlers must verify every delivery's signature before trusting the request.

Send a webhook.test event from the portal. Continue only after your endpoint receives it and returns a 2xx response.

Step 5 — Implement the webhook handler ​

When a customer completes payment, Maytes POSTs checkout.authorized to your endpoint. Read its checkout_uuid and capture immediately while the customer waits on a processing screen. The authorization is auto-voided after 2 minutes.

For this proof of concept, call capture and return 200:

POST /webhooks/maytes
  event = the JSON body
  if event.type == "checkout.authorized":
      POST /api/merchant/v1/checkouts/{event.data.checkout_uuid}/capture
  respond 200

Use the existing Bearer token. An empty {} body captures the full amount:

bash
curl -sX POST https://api.maytes.co/api/merchant/v1/checkouts/$CHECKOUT_UUID/capture \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{}'

Capture is synchronous. A 200 response means the money has moved and is your fulfillment trigger.

Don't ship this handler

This handler trusts every POST request. Anyone who learns the URL could forge checkout.authorized and cause you to fulfill an unpaid order.

A production handler verifies the signature first with the backend SDK:

ts
const event = verifyWebhookSignature(rawBody, signatureHeader, signingSecret);

The same call is available in all seven languages. See Verify the signature for complete examples. The assembled handler adds deduplication and cancellation when you cannot fulfill.

Step 6 — Pay it, and watch it land ​

You now have a working checkout call, webhook subscription, and capture handler.

Create a payable checkout. Repeat the Step 2 request, because the earlier checkout has probably passed its 15-minute window. Export the new identifier:

bash
export CHECKOUT_UUID='the_new_checkout_uuid'

Open its checkout_url in a browser and complete Step 3:

  1. Sign in with your mobile number and enter the SMS code. Use a phone you can access.
  2. Choose how to split the cost.
  3. Pay with the test card 4242 4242 4242 4242 — any future expiry, any CVC (see test cards).

Watch your server logs after submitting the payment:

  1. Your endpoint receives checkout.authorized — the delivery you subscribed to in Step 4.
  2. Your handler captures, and gets status: "captured" back. The money has moved.
  3. The processing screen clears. The customer adds the mates sharing the cost, then lands on the return_url from Step 2.

Fulfill on capture, not on the return_url landing. The customer may spend several minutes adding mates or close the tab before reaching your page. The payment remains valid.

Confirm the status independently, as your support tooling should:

bash
curl -s https://api.maytes.co/api/merchant/v1/checkouts/$CHECKOUT_UUID \
  -H "authorization: Bearer $ACCESS_TOKEN"
json
{
  "data": {
    "checkout_uuid": "019fbb9e-ecd1-7059-99f3-f8bf47b63931",
    "status": "captured",
    "amount": 4500,
    "currency": "AUD",
    "captured_at": "2026-09-14T03:31:12.000Z"
  }
}

"status": "captured" confirms a complete payment.

Nothing arrived at your endpoint?

Check the webhook delivery log in the portal or call GET /webhooks/deliveries. It records each attempt and your endpoint's HTTP response. If the checkout is voided with reason: "auth_expired", your handler did not capture within the 2-minute window.

Go live ​

  1. Run the sandbox pre-go-live checklist — declines, abandonment, cancellation, amount-mismatch errors, and webhook.test against your production endpoint.
  2. Handle failures — respond to error codes, such as invalid_amount_arithmetic and not_capturable, instead of assuming success.
  3. Switch to production — use https://api.maytes.co, your production client_id and client_secret, and a production webhook subscription with its own signing secret.

Next steps ​