Miracle Docs

Cancel / Void

Cancel a payment before it has been captured. In card processing, this is also called a "void" — it releases the authorization hold on the customer's card without charging them.


When to use cancel

  • Customer changed their mind before you fulfilled the order.
  • Order cannot be fulfilled — item out of stock, shipping restriction, etc.
  • Duplicate payment — customer accidentally paid twice.
  • Authorization no longer needed — pre-order canceled, booking withdrawn.

Cancel releases the held funds immediately. The customer sees the hold disappear from their statement (timing depends on their issuing bank).


Cancel a payment

Call POST /v1/payments/{id}/cancel with an empty body:

curl -X POST https://api.miracle.com/v1/payments/pay_abc123/cancel \
  -H "Authorization: Bearer sk_test_your_secret_key" \
  -H "Idempotency-Key: cancel-order-5678"
const response = await fetch('https://api.miracle.com/v1/payments/pay_abc123/cancel', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_test_your_secret_key',
    'Idempotency-Key': 'cancel-order-5678',
  },
});

const { data: payment } = await response.json();
// payment.status === 'canceled'

The response returns the updated Payment object with status: "canceled".


When can you cancel?

Cancel is available only before funds have been charged. The eligible statuses are:

Payment statusCan cancel?What happens
processingYesPayment is marked canceled locally. No provider contact is needed because authorization has not yet been obtained.
requires_actionYesPending customer action (e.g., 3DS) is abandoned.
requires_captureYesA void is sent to the provider to release the authorization hold on the customer's card.
succeededNoFunds already charged. Use refund instead.
capturedNoFunds already charged. Use refund instead.
failedNoPayment already in terminal state.
canceledNoAlready canceled.

Cancel vs refund — decision tree

Use this to decide which operation to call:

Is the payment in `requires_capture`, `processing`, or `requires_action`?
  |
  |-- Yes --> POST /v1/payments/{id}/cancel
  |
  |-- No --> Is it `succeeded` or `captured`?
                |
                |-- Yes --> POST /v1/payments/{id}/refunds
                |
                |-- No --> No action needed (already terminal)

Cannot cancel after capture. Once a payment has been captured or has succeeded (auto-capture), the funds have been charged. You must use a refund to return funds to the customer.

3DS timeout does not cancel the payment. If a payment times out while waiting for the customer to complete a 3DS challenge (requires_action), the payment transitions to failed — not canceled. You will receive a payment.failed webhook with failureReason: '3ds_timeout'. Cancel is only possible while the 3DS challenge is still active.


Auto-cancellation

If a payment in requires_capture is not captured within the authorization validity window (typically 7 days for cards), the authorization expires and the payment is automatically canceled by the system. You receive a payment.canceled webhook when this happens.


Webhook

A successful cancel triggers the payment.canceled webhook event:

{
  "id": "evt_xyz789",
  "type": "payment.canceled",
  "data": {
    "id": "pay_abc123",
    "tenantId": "ten_...",
    "status": "canceled",
    "canceledBy": "merchant",
    "amount": { "currency": "USD", "valueMinor": 5000 },
    "...": "full Payment object"
  },
  "occurredAt": "2026-04-09T14:30:00Z"
}

The data field contains the full Payment object, not a minimal subset. The exact fields included are scoped to the subscriber's access level. The example above is abbreviated for readability.

The canceledBy field indicates who initiated the cancellation: "merchant" (your API call), "system" (authorization expiry or timeout), or "provider" (provider-initiated void).


On this page