Skip to content

Capture & order status

Maytes uses an authorize-then-capture model — the same pattern as a card pre-auth. When the shopper completes the Maytes window, their funds are authorized (held on the card), not yet taken. You capture to finalise the order and move the money. If you can't fulfil, you cancel to release the authorization back to the shopper.


The merchant-side actions

ActionEndpointPurpose
Get checkoutGET /api/merchant/v1/checkouts/{checkout_uuid}Read the current order state.
CapturePOST /api/merchant/v1/checkouts/{checkout_uuid}/captureFinalise an authorized order; charge the shopper.
CancelPOST /api/merchant/v1/checkouts/{checkout_uuid}/cancelVoid an authorized order; release the hold back to the shopper.

Full request/response schemas are in the API reference — this page is the narrative for when to call which, and what to look for in the response.


The order lifecycle

From your code's point of view there are three phases that matter:

PhaseWhat it meansStatus valuesWhat to do
Awaiting paymentShopper hasn't finished paying.created, pendingWait.
AuthorizedFunds are held on the shopper's card. Ready to capture.authorizedCapture (or cancel).
DoneTerminal (or settling into terminal).captured ✅, voided, failed, refunding, refundedIf captured, fulfil. Otherwise the order isn't paid (or has been refunded).

The full status table:

StatusMeaning
createdCheckout created; shopper hasn't paid yet.
pendingShopper is in the Maytes window.
authorizedPayment authorized (funds held). Ready to capture.
capturingCapture in flight. You won't see this as the response to your own /capture call — that call waits for the payment provider to confirm — but a concurrent GET can observe it.
capturedFunds captured. Order is paid. ✅
refundingRefund submitted; awaiting settlement. Normal interim state after /refund.
refundedRefund confirmed. Terminal.
voidedAuthorization released (you cancelled, or the hold expired).
failedPayment failed. Terminal.

Reading the order status

bash
curl -s https://api.maytes.co/api/merchant/v1/checkouts/$CHECKOUT_UUID \
  -H "authorization: Bearer $ACCESS_TOKEN"
json
{
  "data": {
    "checkout_uuid": "0192a8f2-...",
    "merchant_order_id": "order_1001",
    "status": "authorized",
    "amount": 4500,
    "currency": "AUD",
    "captured_at": null,
    "merchant_expires_at": null,
    "transaction_ref": null,
    "checkout_url": "https://checkout.maytes.co/0192a8f2-...",
    "return_url": "https://shop.example.com/thanks?order=order_1001",
    "cancel_url": "https://shop.example.com/cart",
    "service_fee": {
      "gross": 0,
      "discount": 0,
      "net": 0,
      "breakdown": []
    },
    "refunded_amount": 0,
    "merchant_processor_fee": 0,
    "merchant_service_fee": 0,
    "last_refund_at": null
  }
}

status is what you branch on. transaction_ref is set on capture (Maytes' reference for the captured transaction). captured_at flips from null to an ISO timestamp the moment the checkout reaches captured. service_fee is the platform fee shown to the shopper (distinct from your own cart-level fees). The refunded_amount / last_refund_at fields track refunds.

You can pass an optional merchant_order_id as a query parameter — if it doesn't match the checkout's stored value, the request fails with 422 order_mismatch. Use it as a defensive check when looking up a checkout by uuid from a stored mapping.


Implement capture

When to call capture

It depends on your integration flow:

  • Webhook flow (recommended): capture from your checkout.authorized webhook handler, immediately — your customer is watching a payment-processing screen in the Maytes window until you do, and the authorization is auto-voided after 2 minutes — see timing expectations. Capture success is your fulfilment trigger. See the handler recipe.
  • Redirect flow (legacy): capture from your success-page handler after the shopper lands on return_url. The landing is your signal that the shopper authorized.

Either way, capture is server-side only: it uses your OAuth Bearer token, which never leaves your backend.

The capture window is 2 minutes

An authorization that nothing captures is automatically voided after 2 minutes. When that happens, funds release back to the shopper and — if you're subscribed — you receive a checkout.voided webhook with reason: "auth_expired"; a late capture attempt returns 409 not_capturable. Capture immediately when you learn the checkout is authorized — the window is headroom for retries, not a place to park the order. (You can also set your own order-level deadline with merchant_expires_at on create.)

Capture the full order

Call /capture with an empty body and Maytes captures the full authorized amount. We recommend including expected_total_amount and currency so Maytes can assert your idea of the order matches its stored value:

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 '{ "expected_total_amount": 4500, "currency": "AUD" }'
FieldTypeNotes
expected_total_amountintegerAssertion. Mismatch → 422 amount_mismatch.
currencystringAssertion. Mismatch → 422 currency_mismatch.
merchant_order_idstringAssertion. Mismatch → 422 order_mismatch.
metadataobjectEchoed back.

An empty body ({}) captures the full authorized amount with no assertions.

Capture is synchronous. The call waits for the payment provider to confirm before returning — the body you read already has status: "captured", the captured_at timestamp, and the transaction_ref:

json
{
  "data": {
    "checkout_uuid": "0192a8f2-...",
    "merchant_order_id": "order_1001",
    "status": "captured",
    "captured_at": "2026-09-14T03:31:12.000Z",
    "amount": 4500,
    "currency": "AUD"
  }
}

No polling needed — once the call returns 200, the order is captured and you can fulfil.


If you can't fulfil the order

Call cancel before capture to release the authorization back to the shopper's card. The checkout transitions to voided and no funds move. In the webhook flow, cancelling from your handler also releases your customer from the processing screen — don't leave them waiting while you deliberate.

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

You can only cancel while the order is awaiting payment or authorized — once captured, the funds have moved and the API rejects cancel with 409 session_not_voidable. Money already captured goes back via a refund, issued from the merchant portal.


Idempotency & retries

Both capture and cancel are idempotent on terminal states — calling capture on an already-captured checkout returns success without double-charging; calling cancel on an already-voided one returns success without trying to void twice. Safe to retry on network glitches.

This is also what makes migrating between flows safe: if your webhook handler and your legacy success page both attempt capture on the same checkout, the second call is a no-op success. Use your own merchant_order_id (set at create) as the deduplication key end-to-end through your integration, and dedupe webhook deliveries on the envelope id.


Common errors

HTTPcodeOperationCause
409not_capturablecaptureThe checkout isn't in a capturable state (e.g. not yet authorized, or already voided).
409session_not_voidablecancelThe checkout can't be voided — typically already captured.
422amount_mismatchcaptureexpected_total_amount didn't match the authorized amount.
422currency_mismatchcapturecurrency didn't match.
422order_mismatchget / capture / cancelmerchant_order_id didn't match.
422no_active_sessioncaptureThe checkout has no active payment session to capture.
404not_foundallNo such checkout for your merchant account.

Full list per operation in the API reference.


Considerations

  • Server-side only. Capture and cancel use your OAuth Bearer — never expose client_id / client_secret to the browser.
  • Synchronous response. /capture waits for the payment provider — a 200 means captured. (The refund endpoint is the opposite — see Refunds.)
  • The capture window is 2 minutes. Uncaptured authorizations are auto-voided (checkout.voided, reason: "auth_expired"). Capture or cancel immediately.
  • Idempotent. Retries on terminal states are safe, including across two capture paths during a flow migration.
  • Partial capture isn't supported today — capture either captures the full authorized amount or nothing. If you only want to fulfil part of an order, cancel and create a new checkout for the smaller amount.

What's next

  • Webhooks — the checkout.authorized event that triggers capture in the recommended flow.
  • Create a checkout — the step that produces the checkout_uuid you capture here.
  • API reference — every field and error.