# Handle failed refund attempts

**Understand how InStore moves a delegated refund to a fallback refund option when the primary refund method fails, and how a store associate cancels a credit card refund that is in progress.**

This page extends [Set up delegated refunds](/instore/integration/refunds/delegated-refunds.md). It covers two behaviors that decide what happens when a refund attempt doesn't complete:

- [Refund fallbacks](/instore/integration/refunds/failed-refunds.md#refund-fallbacks): what InStore does when an attempt fails and the refund instruction names other payment options.
- [Cancellation](/instore/integration/refunds/failed-refunds.md#cancellation): how the associate cancels a refund, including a credit card refund that is waiting on your payment processor.

## Refund fallbacks

A refund instruction can name alternative payment options in `fallbackRefundTypes`. When the attempt on the primary option fails, the associate can complete the same refund through one of them. For the fields, see [Refund object](/instore/integration/refunds/delegated-refunds.md#refund-object).

### How InStore handles fallbacks

InStore doesn't choose a fallback or run it automatically. Your predetermined refund alternatives are presented during checkout, and the associate decides what happens after a failure.

1. **Session creation.** InStore resolves the primary payment option and every fallback option, and the processors they point to. A reference that doesn't resolve fails the whole request, so no refund session is created. The session response lists each instruction in `refundProcess[]`, with its fallbacks in `refundProcess[].fallbacks[]`.
2. **Identifiers.** A fallback has no `transactionId` of its own. InStore copies `orderId` and `originalCTPaymentId` from the parent instruction into each fallback, so a fallback always refunds the same Order and Payment.
3. **Order of options.** The associate sees the primary option first, followed by the fallbacks in the order you listed them.
4. **One option at a time.** Each refund instruction is attempted through one option at a time. InStore never splits a single instruction across options.

### What counts as a failure

InStore treats each of these as a failed attempt on the active option:

| Cause | Where it comes from |
| --- | --- |
| Your processor sends a [result event](/instore/integration/refunds/delegated-refunds.md#result-events) whose effective status isn't `Success`. | Your processor |
| The associate answers a [prompt](/instore/integration/refunds/delegated-refunds.md#prompts) with anything other than the `primary` option. | The associate |
| The attempt times out, or your processor rejects the request. | Your processor |
| The associate [cancels a credit card refund](/instore/integration/refunds/failed-refunds.md#credit-card-refund-cancellation). | The associate |
| The selected option resolves to no payment processor. | Configuration |
| A cash entry can't be paid out because there isn't enough cash in the drawer. | The drawer |

### What the associate can do after a failure

InStore processes refund entries one at a time, in order. If an entry fails, InStore doesn't attempt the remaining entries until the associate resolves it. After a failure on an entry, the associate sees a dialog for that entry with these choices:

| Choice | Result |
| --- | --- |
| Retry the failed option | InStore sends your processor a new request. Every request carries `additionalData.retryCount` alongside any `pspData` from the refund instruction. The first attempt on an option sends `0`, and each retry adds 1 (1, 2, and so on). |
| One of the untried fallbacks | InStore starts the entry on that option, and `retryCount` resets to `0`. A fallback starts straight away, without another **Next** click. |
| Cancel | The refund is canceled. Shown only while no tender has succeeded. See [When the refund is canceled](/instore/integration/refunds/failed-refunds.md#when-the-refund-is-canceled). |

Options that aren't currently available, for example because their processor failed to set up, are shown but can't be selected.

On the plan screen, before an entry is attempted, the associate can also use **Change method** to start it on a fallback without a failure.

A failed entry blocks the plan. InStore doesn't move on to the next entry until the associate has resolved the failed one.

### Effects on your integration

- **Your processor sees each attempt as a separate refund request.** Each attempt, including a retry, gets its own `requestId`. A fallback to another processor goes to that processor's URL, and a fallback to the same processor is a new request on the same URL.
- **Failed attempts aren't recorded.** InStore records a tender only after an option succeeds, with the `paymentOptionId` of the option that actually succeeded. The [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook) therefore reports the fallback option, not the primary one, when a fallback completed the refund.
- **Only a `Success` result completes a refund.** InStore moves on to a retry or a fallback after any other result, so make sure a failed or canceled attempt hasn't moved money.
- **The `customerId` follows the active option.** Your processor receives the `customerId` of the instruction or fallback that is running. See [Pay-on-account refund processing](/instore/integration/refunds/delegated-refunds.md#pay-on-account-refund-processing).

## Cancellation

A refund can be canceled in two ways. The associate can [cancel the whole refund](/instore/integration/refunds/failed-refunds.md#when-the-refund-is-canceled), and can [cancel a credit card attempt](/instore/integration/refunds/failed-refunds.md#credit-card-refund-cancellation) that is waiting on your processor.

### When the refund is canceled

The associate can cancel a refund until the first tender succeeds. After that, **Cancel** is deactivated on the plan screen and not offered in the failure dialog, because part of the return has already been refunded. A failed entry then has to finish through a retry or a fallback.

When an entry has failed on its primary option and on every fallback, InStore ends the refund process without offering another choice, and dispatches a [`mistStatus`](/instore/implement-instore/integrate-with-instore/events.md#miststatus) event to the host application, with `process` set to `refund` and `status` set to `cancelled`. The associate's own **Cancel** choices dispatch the same event.

> Cash entries are attempted together, as one batch. When the drawer can't cover the batch and another entry has already succeeded, InStore doesn't cancel the refund. It reopens the exhausted cash entries so that the associate can choose a different method for them.

### Credit card refund cancellation

A credit card refund can wait on a card reader for the customer. A store associate can interrupt that wait with **Cancel**, if you allow it for the processor.

#### Enable cancellation

InStore shows **Cancel** on the credit card refund screen only when both of these are true:

- The payment processor is a `BankCard` processor.
- Its `allowCancel` parameter is set to `true` in the [payment processor configuration](/instore/integration/payments/build-payment-components.md#sample-payment-processor-payloads). The parameter is `false` by default.

Before enabling cancellation, configure the [nullified payment receipt template](/instore/implement-instore/receipts/nullified-payment-receipt.md).

Cancellation applies to refunds processed by a `BankCard` processor only. Stored value, wallet, and pay-on-account refunds have no **Cancel** button, and neither does cash.

#### How cancellation works

1. The associate starts the refund. InStore sends the [refund request](/instore/integration/refunds/delegated-refunds.md#refund-webhook) to your processor and receives a `requestId`.
2. The associate taps **Cancel**. The button is on screen for the whole attempt, but a cancel can be sent only once InStore has the `requestId` of the refund request. Before that, InStore shows an error and sends nothing. After a tap, the button is deactivated, so the cancel can't be sent twice.
3. InStore marks the attempt as having a cancel pending, and removes any prompt that is on screen. From now on, any prompt that your processor streams for this attempt is discarded, not shown.
4. InStore posts a cancel request to its own `custom/cancel_credit_payment` endpoint, which forwards it to the `cancel_credit_payment` path on your processor URL.
5. Your processor stops the operation on the card reader, then reports the outcome as the final result of the **original refund request**, on that request's `callbackUrl`.
6. InStore treats the result as a failed attempt, as for any other [failure](/instore/integration/refunds/failed-refunds.md#what-counts-as-a-failure).

The cancel request doesn't end the refund by itself. The refund ends when your processor sends the result for the original request. Until then the associate keeps seeing the refund screen. The processor's `connectionTimeout` limits TCP connection setup. Its `timeout` limits the entire HTTP request, including reading the response body.

> Cash primary refunds and refund fallbacks to Cash are processed together at the end of the sequence.

#### Cancel request

InStore posts the cancel request to `{processorUrl}/cancel_credit_payment`. It's wrapped like the other [payment requests](/instore/implement-instore/custom-payments/payment-extensions.md#prepare-to-receive-requests-from-instore): the fields below arrive in `payload`, next to `requestId`, `statusUrl`, and `callbackUrl`. Verify `X-Signature` as for every other request.

```json title="Sample cancel request payload"
{
  "requestId": "req-1",
  "location": { "typeId": "location", "key": "store-1" },
  "workstation": { "typeId": "workstation", "key": "register-1" },
  "referencePayment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" },
  "cartKey": "return-cart-1",
  "processor": "credit-processor-1",
  "paymentProcessorId": "66f1a2b3c4d5e6f708192a3b",
  "amount": { "amount": 4999, "currency": "USD", "precision": 2 }
}
```

| Element | Data type | Description |
| --- | --- | --- |
| requestId | `String` | The `requestId` of the refund attempt to cancel. It's the one InStore sent in the refund request. |
| location / workstation | `Object` | References to the location and workstation of the refund. |
| referencePayment | `Object` | A reference to the original commercetools [Payment](/search.md?urn=ctp:api:type:Payment) being refunded. |
| cartKey | `String` | The key of the return Cart. |
| processor | `String` | The key of the payment processor. |
| paymentProcessorId | `String` | The ID of the payment processor. |
| amount | `Object` | The refund amount of the attempt, when known, as InStore tender money: `{ amount, currency, precision }`. |

Respond with a `2xx` status code to acknowledge the request. Don't wait for the card reader to stop before you respond. InStore reads a non-`2xx` response, or no response within the timeout, as a failed cancel request: see [If the cancel request fails](/instore/integration/refunds/failed-refunds.md#if-the-cancel-request-fails).

#### Report the cancellation

Send the outcome as a [result event](/instore/integration/refunds/delegated-refunds.md#result-events) on the `callbackUrl` of the original refund request. Use a status other than `Success`, and `Cancelled` for a cancellation:

```json title="Sample canceled refund"
{
  "event": "RefundFailure",
  "data": {
    "status": "Cancelled",
    "transactionId": "req-1",
    "reason": "Refund canceled on the terminal"
  }
}
```

InStore shows `data.reason` to the associate. If you leave it out and `status` is `Cancelled`, the associate sees a generic cancellation message. With any other missing reason, they see a generic decline message.

If the refund had already completed on the reader before your processor received the cancel request, send the `Refunded` result as usual. InStore then records the tender, and the cancel has no effect.

#### What InStore does with the result

A canceled refund follows the same path as a declined one:

- No tender is recorded for the attempt, and nothing is written to commercetools.
- If the workstation is set up to print [nullified payment receipts](/instore/implement-instore/receipts/nullified-payment-receipt.md), InStore prints one for the attempt.
- With fallbacks left on the instruction, the associate sees the failure dialog, described in [What the associate can do after a failure](/instore/integration/refunds/failed-refunds.md#what-the-associate-can-do-after-a-failure).
- With none left, InStore cancels the refund.

#### If the cancel request fails

If InStore can't deliver the cancel request, the refund isn't canceled. InStore:

- Shows an error to the associate.
- Re-enables **Cancel**.
- Restores the prompt that was on screen, if there was one.
- Lets the refund continue, so your processor can still send its result.

#### Checklist for your processor

- Implement `cancel_credit_payment` and verify its `X-Signature`.
- Look up the in-flight operation by `requestId`, and stop it on the card reader.
- Acknowledge the cancel request without waiting for the reader.
- Send a result event for the original refund request, with `status: "Cancelled"` and a `reason`.
- Send exactly one result per refund attempt: a late `Refunded` event after a `Cancelled` one is ignored.
- Make sure a canceled attempt hasn't moved money.

## Related pages

- [Area overview page with navigation](/instore.md)
- [Previous page: Set up delegated refunds](/instore/integration/refunds/delegated-refunds.md)
- [Search documentation and API specs](/search.md)