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 status | Can cancel? | What happens |
|---|---|---|
processing | Yes | Payment is marked canceled locally. No provider contact is needed because authorization has not yet been obtained. |
requires_action | Yes | Pending customer action (e.g., 3DS) is abandoned. |
requires_capture | Yes | A void is sent to the provider to release the authorization hold on the customer's card. |
succeeded | No | Funds already charged. Use refund instead. |
captured | No | Funds already charged. Use refund instead. |
failed | No | Payment already in terminal state. |
canceled | No | Already 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).
Related
- Payment Lifecycle — full status flow and terminal states
- Capture — charge an authorized payment instead of canceling
- Refunds — return funds after capture
- Idempotency — safe retries for cancel calls