---
url: /quickstart.md
---
# Quickstart

Use the [webhook flow](/how-it-works#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.

::: warning 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](/build-your-integration) shows how to implement them with the [backend SDK](/build-your-integration).
:::

Run the guide in the **[sandbox environment](/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](/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](/build-your-integration).
* 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](/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](/authentication) for caching and refresh.

::: tip Use the SDK
The [backend SDKs](/build-your-integration) 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](/create-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](/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](/capture-and-status#when-to-call-capture).

## 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](/webhooks#the-delivery-log) and `webhook.test` button. Portal access is set up during [onboarding](/onboarding#_2-get-access-to-the-merchant-portal).

### 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](/webhooks#verify-the-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.

::: danger 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](/build-your-integration):

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

The same call is available in all seven languages. See [Verify the signature](/webhooks#verify-the-signature) for complete examples. The [assembled handler](/capture-and-status#putting-it-together-your-webhook-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](#step-2-—-create-a-checkout), 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](#step-3-—-send-the-customer-to-the-maytes-window):

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](/sandbox-environment#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.

::: tip Nothing arrived at your endpoint?
Check the **webhook delivery log** in the portal or call [`GET /webhooks/deliveries`](/webhooks#the-delivery-log). 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](/capture-and-status#when-to-call-capture).
:::

## Go live

1. **Run the [sandbox pre-go-live checklist](/sandbox-environment#what-to-test-before-going-live)** — declines, abandonment, cancellation, amount-mismatch errors, and `webhook.test` against your production endpoint.
2. **Handle failures** — respond to [error codes](/api-reference), 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

* Read [Webhooks](/webhooks) for signature verification, delivery semantics, and secret rotation.
* Read [Capture & checkout status](/capture-and-status) for the status lifecycle, cancel, and idempotency.
* Build richer carts with [line items, fees, discounts, and tax](/create-checkout).
* Drop the [checkout button](/checkout-button) onto your online store.
