# Set up delegated refunds Understand how InStore delegates returns and refunds to your integration, and implement the webhook notifications your integration receives. ## Set up delegated refunds Setting up delegated refunds is similar to setting up [delegated payments](/instore/integration/payments/delegated-payments.md). You do not need to implement [delegated payments](/instore/integration/payments/delegated-payments.md) to implement delegated refunds. When InStore receives a delegated refund request, it records the tender against the session. For every tender your processor handles, it does not create or reverse a commercetools [Payment](/search.md?urn=ctp:api:type:Payment) transaction on your behalf. Your payment processor integration remains responsible for reversing the original charge. [Cash tenders](/instore/integration/refunds/delegated-refunds.md#cash-refund-processing) are the exception. For the processor endpoint and payload, see [Refund webhook](/instore/integration/refunds/delegated-refunds.md#refund-webhook). InStore provides for delegation of these refund types: - [Credit](/instore/integration/refunds/delegated-refunds.md#credit-refund-processing) (`BankCard`) refunds - [Stored value](/instore/integration/refunds/delegated-refunds.md#stored-value-refund-processing) (`GiftCard`) refunds - [Wallet](/instore/integration/refunds/delegated-refunds.md#wallet-refund-processing) (`Wallet`) refunds - [Pay-on-account](/instore/integration/refunds/delegated-refunds.md#pay-on-account-refund-processing) (`PayOnAccount`) refunds [Cash tenders](/instore/integration/refunds/delegated-refunds.md#cash-refund-processing) can be included in a delegated refund plan, but they are handled differently from other tender types. A delegated refund moves through four objects in your systems: | Object | Direction | | --- | --- | | [Refund object](/instore/integration/refunds/delegated-refunds.md#refund-object) | Your host application to InStore | | [Refund request](/instore/integration/refunds/delegated-refunds.md#refund-webhook) | InStore to your payment processor | | [Finalize notification](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook) | InStore to your webhook service | | [Receipt data request](/instore/integration/refunds/delegated-refunds.md#refund-receipt-data-webhook) | InStore to your webhook service | ### How to read this page Every field table on this page has a **Presence** column. What it means depends on the direction of the payload, which is stated at the start of each section: some payloads you send to InStore, and some InStore sends you. - **Required**: the field is always present. In a payload you send to InStore, you must include it or InStore rejects the request. In a payload InStore sends you, InStore always includes it. - **Optional**: the field can be absent. In a payload you send to InStore, you can omit it. In a payload InStore sends you, InStore only includes it under the conditions described. Absent fields are omitted from the JSON entirely rather than sent as `null`, so write your handlers so that a missing key and an empty value behave the same way. #### Money and reference shapes Three different money shapes appear during a refund. Which one you get depends on where the value originated, not on the endpoint, so check the shape per field rather than per payload. | Shape | Structure | Used by | | --- | --- | --- | | Refund instruction money | `{ amount, currency }` | `refundAmount` in the [refund object](/instore/integration/refunds/delegated-refunds.md#refund-object), which your host application sends to InStore. | | InStore tender money | `{ amount, currency, precision }` | `data.tenderItems[].amount` in the [receipt data request](/instore/integration/refunds/delegated-refunds.md#refund-receipt-data-webhook), which InStore sends to you. `precision` is the number of decimal digits. | | REST API money | `{ currencyCode, centAmount, fractionDigits }` | Everything InStore sends to you: to your payment processor and in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook). InStore is the sender, but the shape matches the REST API [CentPrecisionMoney](/search.md?urn=ctp:api:type:CentPrecisionMoney) type. | Two different reference shapes also appear: | Shape | Structure | Used by | | --- | --- | --- | | REST API reference | `{ typeId, key }` or `{ typeId, id }` | References to commercetools resources and to InStore payment options. The shape matches a REST API [Reference](/search.md?urn=ctp:api:type:Reference) or [ResourceIdentifier](/search.md?urn=ctp:api:type:ResourceIdentifier). Send `key` or `id`; you can send both. Where InStore resolves a reference, it prefers `key`. | | Scope reference | `{ type, key }` | `tenant`, `location`, and `workstation` in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook) only. This shape is specific to InStore, not the REST API. | ### Refund object When a store associate starts a return, your host application passes a refund object to the InStore refund component, in the format shown below. It describes the return [Cart](/search.md?urn=ctp:api:type:Cart), the return [Orders](/search.md?urn=ctp:api:type:Order) and [Line Items](/search.md?urn=ctp:api:type:LineItem), and one or more refund instructions. Each refund instruction names a payment option and the original commercetools [Payment](/search.md?urn=ctp:api:type:Payment) it refunds. It can also name fallback payment options to try when the primary option cannot complete the refund. InStore adds the location and workstation, creates the refund session from the object, and resolves the payment options, processors, and totals the associate needs to complete the return. ```json title="Sample refund object" { "returnCart": { "typeId": "cart", "key": "cart-1" }, "returnOrders": [ { "order": { "typeId": "order", "key": "order-1" }, "returnLineItem": [{ "typeId": "line-item", "id": "193eb21f-a5e7-4327-a6c9-705282da9f30" }] } ], "refunds": [ { "refundAmount": { "amount": 4999, "currency": "USD" }, "paymentOption": { "typeId": "paymentOption", "key": "option-payment-key" }, "transactionId": "451sd5f1", "order": { "typeId": "order", "key": "order-1" }, "originalCTPayment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "pspData": { "originalPaymentMethodName": "Mastercard 4444", "simulateMode": "async", "simulatePrompt": true }, "metaData": { "prompt": "Refund will be made to original payment", "promptInfo": "Mastercard ending 4444: -$50.00" }, "fallbackRefundTypes": [ { "paymentOption": { "typeId": "paymentOption", "key": "option-credit-us-1" }, "pspData": {}, "metaData": { "prompt": "Refund will be made to original payment", "promptInfo": "Mastercard ending 4444: -$50.00" }, "processingSequence": 0.9 } ] } ], "idempotencyKey": "cart-1-attempt-1" } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `returnCart` | Object | Required | A reference to the Cart created based on the items refunded. InStore uses this to calculate totals and to get information for the refund receipt. Without it, InStore can't create the refund session. | | `returnOrders` | Array | Required | The Orders that contain the items being returned. Send an empty array if you have none. | | `returnOrders[].order` | Object | Required | A reference to the Order related to the returned Line Items. Contains `typeId`, and `key` or `id`. | | `returnOrders[].returnLineItem` | Array | Required | The Line Items from the Order that are being returned. Send an empty array if you don't itemize the return. | | `refunds` | Array | Required | The refund instructions to be processed. InStore walks them in the order you list them. | | `refunds[].refundAmount` | Object | Required | The refund amount, as refund instruction money. | | `refunds[].paymentOption` | Object | Required | The payment option selected for the refund. Contains `typeId`, and `key` or `id`. InStore resolves it and fails the whole session if it doesn't exist. | | `refunds[].order` | Object | Required | A reference to the Order associated with this refund. | | `refunds[].originalCTPayment` | Object | Required | A reference to the original commercetools [Payment](/search.md?urn=ctp:api:type:Payment) used for the purchase. InStore fetches it to report the original payment amount, and fails the session if it doesn't exist. | | `refunds[].transactionId` | String | Optional | The identifier of the original payment transaction to refund against. A commercetools [Payment](/search.md?urn=ctp:api:type:Payment) can carry multiple prior refunds, so this tells InStore which transaction to refund. When you omit it, InStore falls back to the `originalCTPayment` identifier as the `originalRefundTransactionId` it reports in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook). | | `refunds[].pspData` | Object | Optional | Payment service provider data related to the refund. Can have any value, with one key InStore reads: `originalPaymentMethodName`. InStore displays that value as the original payment method, and records it as the payment method on a cash refund tender. The whole object, that key included, is also passed through untouched to your processor as `additionalData`, for your processor to interpret. In the sample above, `simulateMode` and `simulatePrompt` are keys the processor defines: they tell it to answer asynchronously and to raise an operator prompt before completing the refund. | | `refunds[].metaData` | Object | Optional | Display metadata used to communicate refund information to the user. When you omit it, the refund screen shows no prompt text. | | `refunds[].metaData.prompt` | String | Optional | The main prompt message shown for the refund. | | `refunds[].metaData.promptInfo` | String | Optional | Additional prompt information, such as masked card details and the refund amount. InStore shows it on the refund summary when you send it. | | `refunds[].customerId` | String | Optional | Applies to [pay-on-account refunds](/instore/integration/refunds/delegated-refunds.md#pay-on-account-refund-processing) only. The customer account to credit the refund to. InStore expects it to be optional, so the field is never required and a refund instruction without it is still valid, but a pay-on-account refund can't identify the account to credit without it. Omit it for every other refund type. | | `refunds[].fallbackRefundTypes` | Array | Optional | Fallback refund options to use if the primary refund option cannot be processed. A refund can use a configured credit option as a fallback. | | `refunds[].fallbackRefundTypes[].paymentOption` | Object | Required | The fallback payment option. InStore resolves it at session creation, like the primary one. | | `refunds[].fallbackRefundTypes[].pspData` | Object | Optional | As `refunds[].pspData`, for this fallback. | | `refunds[].fallbackRefundTypes[].metaData` | Object | Optional | As `refunds[].metaData`, for this fallback. | | `refunds[].fallbackRefundTypes[].processingSequence` | Float (0-1) | Optional | The intended priority of this fallback option. InStore stores the value with the refund session, but doesn't use it to order the fallbacks: they're offered in the order you list them in `fallbackRefundTypes`. | | `refunds[].fallbackRefundTypes[].customerId` | String | Optional | As `refunds[].customerId`. Send it when the fallback is a pay-on-account option. | | `idempotencyKey` | String | Optional | A key that makes session creation repeatable. When InStore has already created a session for this key on your tenant, it returns that session instead of creating a second one. Send it if the associate can retry a return that failed mid-flight. | A fallback carries no `transactionId` of its own. It refunds the same Order and commercetools Payment as the refund instruction it replaces, so InStore copies those identifiers down from the parent instruction. #### How the refund types differ Every processor-mediated refund type uses the same path on your processor URL: `{processorUrl}/refund`. One handler serves all four types. The main differences are the [`paymentMethod` variant](/instore/integration/refunds/delegated-refunds.md#paymentmethod-variants) InStore sends, the tender type it records, and whether your processor is set up before the refund starts: | Payment processor type | `paymentMethod.type` | Tender recorded as | Setup before the refund | | --- | --- | --- | --- | | `BankCard` | `BankCard` | `Credit` | `initialize_credit_processor` and `connect_card_reader`, according to the setup steps configured on the processor. | | `GiftCard` | `GiftCard` | `GiftCard` | None. | | `Wallet` | `Wallet` | `Wallet` | None. | | `PayOnAccount` | `PayOnAccount` | `POA` | None. | Apart from these differences, the request body is identical across the four types. You can route a refund on `paymentMethod.type` alone, without resolving the payment option first. [Cash](/instore/integration/refunds/delegated-refunds.md#cash-refund-processing) isn't in this table: it reaches no processor, so it has no `paymentMethod` variant. It's recorded as a `Cash` tender and reported to you in the finalize and receipt data payloads only. #### Credit refund processing The delegated refunds feature supports refunding to and from credit cards, debit cards, and other tenders that are of the `BankCard` tender type, including stored value cards that are processed through a PED. This is the only type whose processor is initialized and connected to a PED before the refund starts. For those setup calls, see [Processor webhook paths that we provide for specific payment types](/instore/implement-instore/custom-payments/payment-extensions.md#processor-webhook-paths-that-we-provide-for-specific-payment-types). The refund itself goes to your `refund` path, and the tender is recorded as `Credit`. ##### Configure the Cancel button For delegated refunds that involve a Credit tender, you can configure a **Cancel** button for display during the credit stepper. The store associate can cancel a refund that has not completed. When selected, the button triggers the [`cancel_credit_payment`](/instore/implement-instore/custom-payments/payment-extensions.md#processor-webhook-paths-that-we-provide-for-specific-payment-types) webhook path, the same route used to cancel payments. For configuration details, including the required receipt template, see the `allowCancel` property in [Build payment components](/instore/integration/payments/build-payment-components.md#sample-payment-processor-payloads). #### Stored value refund processing The delegated refunds feature supports refunding to and from stored value cards that are of the `GiftCard` tender type. The refund goes to your `refund` path with `paymentMethod: { "type": "GiftCard" }`, and the tender is recorded as `GiftCard`. Identify the card from `referencePayment` and your own records of the original payment. #### Wallet refund processing The delegated refunds feature supports refunding to digital wallets that are of the `Wallet` tender type. The refund goes to your `refund` path with `paymentMethod: { "type": "Wallet" }`, and the tender is recorded as `Wallet`. Identify the wallet from `referencePayment` and your own records of the original payment. #### Pay-on-account refund processing The delegated refunds feature supports refunding to customer accounts that are of the `PayOnAccount` tender type. The refund goes to your `refund` path with `paymentMethod: { "type": "PayOnAccount", "customerId" }`, and the tender is recorded as `POA`. Pay-on-account is the only refund type that uses `customerId`. Send it on the refund instruction, or on the fallback when the fallback is the pay-on-account option: ```json title="Sample pay-on-account refund instruction" { "refundAmount": { "amount": 2000, "currency": "USD" }, "paymentOption": { "typeId": "paymentOption", "key": "option-poa-us-1" }, "order": { "typeId": "order", "key": "order-1" }, "originalCTPayment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "customerId": "cust-8842" } ``` `customerId` is optional everywhere it appears in the contract, and InStore never rejects a refund for omitting it. What it does with it, when you send it: 1. Sends the `customerId` from the refund instruction being processed, or from the fallback when a fallback is running, to your processor as `paymentMethod.customerId`. Only the `PayOnAccount` [variant](/instore/integration/refunds/delegated-refunds.md#paymentmethod-variants) carries the field. 2. Stores the first `customerId` it finds across the refunds and their fallbacks on the refund session. 3. Reports that stored value back to you as `tenderInformation.customerId` in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook), on `POA` tenders only. These are two different lookups. The value your processor receives belongs to the instruction being processed; the value the webhook reports is the first one found anywhere on the session. Send the same account on every instruction in a return to keep them aligned. Because the field is optional, a pay-on-account refund instruction that omits `customerId` reaches your processor with no account to credit. Always send it on pay-on-account refunds, and reject the request in your processor when it's missing rather than crediting a default account. #### Cash refund processing Cash tenders use a separate refund process, and it's the one refund type your payment processor isn't involved in. The store associate pays the refund out of the drawer configured for the workstation in InStore, and InStore records the tender and a cash-drawer movement itself. Your `refund` path is never called for a cash tender, so there's no `paymentMethod` variant for cash and nothing for your processor to implement. Cash is also the only refund type for which InStore writes back to commercetools. At finalization, for each cash tender that references an original [Payment](/search.md?urn=ctp:api:type:Payment), InStore adds a `Refund` [Transaction](/search.md?urn=ctp:api:type:Transaction) to that Payment, in state `Success` and with a negative `centAmount`. InStore posts it once per tender: finalizing a session that is already `Refunded` adds nothing further. A cash tender that references no original Payment is still recorded and reported to you, but produces no commercetools Transaction. A cash tender then appears in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook) alongside any credit, stored value, wallet, or pay-on-account tenders on the same return, with a `Cash` `tenderInformation` block. It also appears in the [receipt data request](/instore/integration/refunds/delegated-refunds.md#refund-receipt-data-webhook) as a `tenderItems[]` entry. Those two payloads are where your integration picks up a cash refund to keep its own systems in sync. ### Refund webhook For every processor-mediated tender, InStore posts to the `refund` path on the URL configured for the payment processor. This request differs from the payment requests described in [Set up payment extensions](/instore/implement-instore/custom-payments/payment-extensions.md). InStore posts the body below directly instead of wrapping it in the shared request fields, and beyond `Content-Type` it sends only the `X-Signature` and `X-Correlation-Id` headers. No `X-API-Key` or `X-Transaction-Id` header is included on this request, so do not make those a condition of accepting it. Verify `X-Signature` as an HMAC-SHA256 of the exact request body, using the `apiKey` you configured for the processor. InStore waits for the `timeout` you configured for the processor. ```http POST {processorUrl}/refund ``` ```json title="Sample refund request" { "callbackUrl": "https://api.example.com/proj/instore-tenants/acme/payment-messages?type=result&streamId=req-1&nonce=...", "statusUrl": "https://api.example.com/proj/instore-tenants/acme/payment-messages?type=status&streamId=req-1&nonce=...", "requestId": "req-1", "location": { "typeId": "location", "key": "01" }, "locationKey": "01", "workstation": { "typeId": "workstation", "key": "001" }, "workstationKey": "001", "referencePayment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "amount": { "currencyCode": "USD", "centAmount": 4999, "fractionDigits": 2 }, "paymentMethod": { "type": "BankCard", "device": { "id": "ped-1", "model": "P400", "registrationCode": "R-77" }, "deviceId": "ped-1" }, "additionalData": { "originalPaymentMethodName": "MasterCard 4444", "simulateMode": "async", "simulatePrompt": true } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `callbackUrl` | String | Required | Where to post the final result. Its exact form depends on whether long polling is enabled for the location, so treat it as an opaque URL and post to it verbatim. | | `statusUrl` | String | Required | Where to post intermediate messages, prompts, and input requests. Also opaque. | | `requestId` | String | Required | Identifies this refund attempt. Use it to correlate your own records, and return it on the `resolve` path. | | `location` | Object | Required | A reference to the location. Carries `key` or `id`, whichever InStore held. | | `locationKey` | String | Required | The same value as a plain string: `location.key`, or `location.id` when the reference carried no key. A reference must carry one of the two, so this is always sent. | | `workstation` | Object | Required | A reference to the workstation. | | `workstationKey` | String | Required | As `locationKey`, for the workstation. | | `referencePayment` | Object | Required | A reference to the original commercetools [Payment](/search.md?urn=ctp:api:type:Payment) being refunded. | | `amount` | Object | Required | The refund amount, as commercetools money. Positive. | | `paymentMethod` | Object | Required | How the refund is processed. See [paymentMethod variants](/instore/integration/refunds/delegated-refunds.md#paymentmethod-variants). | | `additionalData` | Object | Optional | The refund instruction's `pspData`, passed through unchanged. Sent only when the instruction carried one. | Respond either synchronously with the refund result, or with a `202` status code and then drive the refund through the [events below](/instore/integration/refunds/delegated-refunds.md#status-events). For the two response shapes, see [Result events](/instore/integration/refunds/delegated-refunds.md#result-events). #### paymentMethod variants InStore chooses the variant from the payment processor's `integrationConfiguration.type`. | Processor type | `paymentMethod` | Notes | | --- | --- | --- | | `BankCard` | `{ "type": "BankCard", "device": { "id", "model", "registrationCode" }, "deviceId" }` | The default variant: InStore sends it for `BankCard` and for any other PED-driven processor type. `device.id` is required. `device.model` and `device.registrationCode` are optional, and sent only when the workstation's active PED defines them. `deviceId` repeats `device.id` and is always sent for this variant. | | `GiftCard` | `{ "type": "GiftCard" }` | Carries no further fields. | | `Wallet` | `{ "type": "Wallet" }` | Carries no further fields. | | `PayOnAccount` | `{ "type": "PayOnAccount", "customerId": "cust-8842" }` | `customerId` is optional, and sent only when the refund instruction or its fallback carried one. This is the only variant that carries the field. See [Pay-on-account refund processing](/instore/integration/refunds/delegated-refunds.md#pay-on-account-refund-processing). | #### Status events Once you've accepted a refund with `202`, InStore waits for you to drive it to completion. Post progress and prompts to the request's `statusUrl`, then exactly one [result](/instore/integration/refunds/delegated-refunds.md#result-events) to its `callbackUrl`. ```http POST {statusUrl} ``` Every event, on either URL, uses the same envelope: an `event` name and a `data` object. Both are required. InStore skips any event where `event` isn't a string or `data` isn't an object, so send `data` even when it's empty. All refund status events use the event name `PaymentRefundUpdate`, and `data.kind` selects what InStore does with the event. ```json title="Sample progress message" { "event": "PaymentRefundUpdate", "data": { "kind": "message", "message": "Contacting payment network", "severity": "info" } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `event` | String | Required | Always `PaymentRefundUpdate` for a status event. | | `data.kind` | String | Required | `message`, `prompt`, or `input`. InStore logs and ignores any other value. | | `data.keepAlive` | Number | Optional | Milliseconds. Replaces the remaining timeout for this refund attempt with a fresh window of this length, so a long step doesn't trip the timeout configured on the processor. Valid on any `kind`. | | `data.message` | String | Optional | The text InStore shows the associate while the refund runs. InStore displays an empty message when you omit it, so send one on every `message` event. | | `data.severity` | String | Optional | Forwarded to the InStore POS with the message. | | `data.properties` | Object | Optional | Free-form data forwarded to the InStore POS with a `message` event. | | `data.requestId` | String | Optional | For `prompt`, InStore falls back to the `requestId` of the attempt in flight when you omit it, which is correct as long as you raise one prompt at a time. For `input` there's no fallback: see [Input requests](/instore/integration/refunds/delegated-refunds.md#input-requests). | | `data.title`, `data.message`, `data.options`, `data.primary` | Various | Optional | For `prompt`. See [Prompts](/instore/integration/refunds/delegated-refunds.md#prompts). | | `data.inputType`, `data.inputData`, `data.callbackUrl` | Various | Optional | For `input`. See [Input requests](/instore/integration/refunds/delegated-refunds.md#input-requests). | `keepAlive` sits inside `data` and is in milliseconds. It isn't the same field as the `keepAliveMs` used by the [payment status callback](/instore/implement-instore/custom-payments/payment-extensions.md#dynamically-replace-the-timeout-for-an-asynchronous-payment). #### Prompts To ask the associate something during a refund, such as confirming the refund on the terminal, send a status event with `kind: "prompt"`. InStore shows a dialog and posts the answer to the `resolve` path on your processor URL. ```json title="Sample prompt" { "event": "PaymentRefundUpdate", "data": { "kind": "prompt", "keepAlive": 20000, "title": "Confirm Refund", "message": "Operator must confirm the refund on the terminal", "options": ["Accept", "Decline"], "primary": "Accept" } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `data.title` | String | Optional | The dialog title. InStore shows an empty title when you omit it. | | `data.message` | String | Optional | The question put to the associate. | | `data.options` | Array | Optional | The answers offered. Each string is both the button label and the value InStore sends back. When you omit it or send an empty array, InStore offers a single option: `primary`, or `Accept` when you sent no `primary` either. | | `data.primary` | String | Optional | Which of `options` means "proceed with the refund." | | `data.keepAlive` | Number | Optional | Milliseconds to wait for the answer. Send one sized to how long an associate realistically takes, or the refund can time out while the dialog is still open. | `primary` decides the outcome, not just the styling. InStore treats the answer matching `primary` as "continue," and **every other answer as a decline**: it fails the refund attempt locally and moves to the next fallback. If `primary` is missing, or isn't one of `options`, then no answer counts as "continue" and every answer declines the refund. InStore posts the answer to your `resolve` path, signed the same way as the refund request: ```http POST {processorUrl}/resolve ``` ```json title="Sample resolve request" { "location": { "typeId": "location", "key": "01" }, "locationKey": "01", "workstation": { "typeId": "workstation", "key": "001" }, "workstationKey": "001", "referencePayment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "paymentProcessorId": "691bb93ff14fd895922ceb16", "requestId": "req-1", "response": "Accept" } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `location` / `workstation` / `referencePayment` | Object | Required | The same references as on the refund request. | | `locationKey` / `workstationKey` | String | Required | The plain-string forms, as on the refund request. | | `paymentProcessorId` | String | Required | The processor the prompt belongs to. | | `requestId` | String | Required | The refund attempt the prompt belongs to. Match it against the attempt you're holding open. | | `response` | String | Required | The answer the associate chose, exactly as you listed it in `options`. When you omitted `options`, it's the single option InStore synthesized instead: your `primary`, or `Accept` when you sent no `primary` either. Branch on this value with that fallback in mind, since it can be a string you never listed. | | `additionalData` | Object | Optional | Passed through when the InStore POS supplies one. | Respond with a `2xx` status code and any body: InStore relays both to the InStore POS unchanged. A non-`2xx` response doesn't reach the InStore POS as-is; it surfaces there as an error. Answering a prompt doesn't end the refund. When the answer is the `primary` one, the refund is still running and InStore waits for your [result event](/instore/integration/refunds/delegated-refunds.md#result-events) on `callbackUrl`. Without it, the attempt runs to timeout. When the answer is anything else, InStore has already failed the attempt and moved to the next fallback, and a result arriving afterwards changes nothing on the InStore side. #### Input requests `kind: "input"` asks the host application for a value mid-refund, instead of putting a dialog in front of the associate. ```json title="Sample input request" { "event": "PaymentRefundUpdate", "data": { "kind": "input", "requestId": "req-1", "inputType": "signature", "inputData": {}, "callbackUrl": "https://processor.example.com/refund/req-1/input" } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `data.inputType` | String | Optional | What you're asking for. The host application decides how to collect it. | | `data.inputData` | Object | Optional | Context so that the host application can collect the value. | | `data.callbackUrl` | String | Optional | Where the host application sends the collected value. This is your own endpoint, carried in the event. | | `data.requestId` | String | Optional | The refund attempt this belongs to. Unlike a prompt, InStore applies no fallback here: it forwards whatever you send, so omit it only if the host application can work without it. | An input request behaves differently from a [prompt](/instore/integration/refunds/delegated-refunds.md#prompts). InStore renders nothing and answers nothing: it raises the request in the host application and passes these four values straight through. The host application collects the value and posts it to the `callbackUrl` in the event, not to your `resolve` path, so InStore is out of the loop for the answer. Only send `kind: "input"` if the host application deployed in your stores handles InStore input requests. Nothing in InStore answers one, so if the host application ignores it the refund sits until the attempt times out. #### Result events ```http POST {callbackUrl} ``` Post exactly one result per refund attempt. InStore stops accepting events for the attempt as soon as one arrives. ```json title="Sample successful refund" { "event": "Refunded", "data": { "payment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "transactionId": "0f6d6b4f-9b3f-4f26-9f0a-2a0f6b2b8a11", "label": "Refund", "amount": { "currencyCode": "USD", "centAmount": 4999, "fractionDigits": 2 } } } ``` ```json title="Sample declined refund" { "event": "RefundFailure", "data": { "status": "Declined", "transactionId": "req-1", "reason": "Operator declined the refund" } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `event` | String | Required | `Refunded` marks the refund approved. Any other name, for example `RefundFailure`, marks it declined. | | `data.status` | String | Optional | Overrides the outcome taken from `event`. InStore carries on only when the effective status is exactly `Success`, and treats anything else as a decline, moving to the next fallback. | | `data.reason` | String | Optional | Why the refund was declined. InStore shows it to the associate; without it, the associate sees a generic decline message. | | `data.label` | String | Optional | The method label. InStore records it as the tender's payment method, which is what appears on the refund receipt. | | `data.transactionId` | String | Optional | Your identifier for the refund. InStore doesn't store it, so it doesn't reappear in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook). Keep your own mapping from `requestId`. | | `data.amount` | Object | Optional | Informational. The actual amount refunded. commercetools does not store this value automatically: record it yourself if you need it. InStore records only the amount it asked you to refund. | | `data.payment` | Object | Optional | Informational. InStore records the Payment from the refund instruction. | `data.status` wins over `event`. A `Refunded` event carrying `"status": "Declined"` is treated as a decline, and a `RefundFailure` event carrying `"status": "Success"` is treated as an approved refund. Send `status` only when you mean to set the outcome with it. ### Finalize refund webhook When the refund session is finalized, InStore sends a webhook notification to the webhook URL configured for your tenant, so you can update your own systems. InStore signs this request the same way it signs other webhook requests. For more information about these, see [Header information that you receive in all InStore payment requests](/instore/implement-instore/custom-payments/payment-extensions.md#header-information-that-you-receive-in-all-instore-payment-requests). In addition to `X-Signature` and `X-Correlation-Id`, this request also includes an `x-tenant-id` header that identifies the tenant, and an `X-API-Key` header when your tenant has a webhook API key configured. ```json title="Sample finalize refund webhook payload" { "tenant": { "type": "tenant", "key": "acme-retail" }, "location": { "type": "location", "key": "01" }, "workstation": { "type": "workstation", "key": "001" }, "action": "Refund", "data": { "refundSessionId": "6a3c08d503353f78bbe2c08f", "refundTransactions": [ { "paymentOption": { "typeId": "paymentOption", "id": "option-credit-us-1" }, "amount": { "currencyCode": "USD", "centAmount": 4999, "fractionDigits": 2 }, "originalRefundTransactionId": "txn_original_payment_1", "tenderInformation": { "type": "Credit", "tenderItemId": "6a3c091003353f78bbe2c090", "payment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" } } }, { "paymentOption": { "typeId": "paymentOption", "id": "option-poa-us-1" }, "amount": { "currencyCode": "USD", "centAmount": 2000, "fractionDigits": 2 }, "originalRefundTransactionId": "txn_original_payment_1", "tenderInformation": { "type": "POA", "tenderItemId": "6a3c091003353f78bbe2c092", "payment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "customerId": "cust-8842" } }, { "paymentOption": { "typeId": "paymentOption", "id": "option-cash-us-1" }, "amount": { "currencyCode": "USD", "centAmount": 500, "fractionDigits": 2 }, "originalRefundTransactionId": "txn_original_payment_1", "tenderInformation": { "type": "Cash", "tenderItemId": "6a3c091003353f78bbe2c091", "roundedRemainder": { "currencyCode": "USD", "centAmount": -1, "fractionDigits": 2 } } } ] } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `tenant` / `location` / `workstation` | Object | Required | Scope references, as `{ type, key }`. | | `action` | String | Required | Always `Refund` for this notification. Branch your webhook handler on this value alongside the `payment`, `refund`, and `receipt` actions you already handle. The lowercase `refund` action belongs to the older refund flow and carries an entirely different body. | | `data.refundSessionId` | String | Required | The internal identifier of the InStore refund session that was finalized. Use it to correlate with the [receipt data request](/instore/integration/refunds/delegated-refunds.md#refund-receipt-data-webhook). | | `data.refundTransactions` | Array | Required | One entry per tender that made up the refund, in the order they were finalized. A session can have multiple entries when a return is split between the original payment method and a fallback tender. | | `data.refundTransactions[].paymentOption` | Object | Required | A reference to the [payment option](/instore/integration/payments/overview.md#payment-options) the tender was recorded against. Contains `typeId` and `id`. The `id` carries whichever identifier InStore recorded on the refund session, which is the option's `key` when the refund instruction referenced it by key. `id` is optional within the object, but a tender recorded through the refund flow always carries a payment option, so it's always present in practice. | | `data.refundTransactions[].amount` | Object | Required | The tender amount, as commercetools money. `fractionDigits` is optional and absent when the tender was recorded without a precision. | | `data.refundTransactions[].originalRefundTransactionId` | String | Required | The identifier of the original payment transaction that this tender refunds. Use this to match the refund back to the payment you are reversing. | | `data.refundTransactions[].tenderInformation` | Object | Required | Tender-specific data for the tender being refunded. Shape depends on the tender type. | | `data.refundTransactions[].tenderInformation.type` | String | Required | The tender type: `Cash`, `Credit`, `Wallet`, `GiftCard`, or `POA`. | | `data.refundTransactions[].tenderInformation.tenderItemId` | String | Required | The internal identifier of the InStore tender. Present for every tender type. Matches `data.tenderItems[]._id` in the receipt data request. | | `data.refundTransactions[].tenderInformation.payment` | Object | Required for processor-mediated tenders | A reference to the commercetools [Payment](/search.md?urn=ctp:api:type:Payment) the tender refunds. Contains `typeId` and `id`, where `id` is the tender's `external_payment_id` when one was recorded, otherwise its `payment_id`. `id` is optional within the object, and absent when the tender carried neither. | | `data.refundTransactions[].tenderInformation.roundedRemainder` | Object | Optional, `Cash` only | The cash-rounding adjustment applied to the refund, as commercetools money. May be negative. Sent whenever InStore recorded a rounding value for the tender, with a `centAmount` of `0` when the refund needed no rounding, and absent when the tender recorded none at all. | | `data.refundTransactions[].tenderInformation.customerId` | String | Optional, `POA` only | The customer account the refund was credited to, taken from the refund session. Absent when no refund instruction or fallback carried one, since [`customerId` is optional](/instore/integration/refunds/delegated-refunds.md#pay-on-account-refund-processing). No other tender type carries this field. | The `tenderInformation` shapes are: | `type` | Fields | Notes | | --- | --- | --- | | `Cash` | `tenderItemId`, `roundedRemainder` | Cash carries no `payment` object. When the tender referenced an original Payment, InStore added the refund Transaction to it at finalization. | | `Credit` | `tenderItemId`, `payment` | The default for any processor-mediated tender that isn't a wallet, stored value, or pay-on-account refund. | | `Wallet` | `tenderItemId`, `payment` | Identical to `Credit` apart from the discriminator. | | `GiftCard` | `tenderItemId`, `payment` | Identical to `Credit` apart from the discriminator. | | `POA` | `tenderItemId`, `payment`, `customerId` | The only shape that adds `customerId`, and only when the refund supplied one. | For delegated `Credit`, `GiftCard`, `Wallet`, and `POA` tenders, InStore does not reverse a commercetools [Payment](/search.md?urn=ctp:api:type:Payment) or [Transaction](/search.md?urn=ctp:api:type:Transaction). Your payment processor integration remains responsible for the reversal. For a cash tender, InStore adds the refund transaction to the commercetools [Payment](/search.md?urn=ctp:api:type:Payment) before sending this webhook. For more information, see [Credit](/instore/integration/refunds/delegated-refunds.md#credit-refund-processing), [Stored value](/instore/integration/refunds/delegated-refunds.md#stored-value-refund-processing), [Wallet](/instore/integration/refunds/delegated-refunds.md#wallet-refund-processing), and [Pay-on-account](/instore/integration/refunds/delegated-refunds.md#pay-on-account-refund-processing) refund processing. InStore sends this webhook on a best-effort basis and does not wait for your response before completing the return in the InStore POS. Respond with a `200` status code to acknowledge receipt. If your webhook service doesn't acknowledge the request, InStore retries up to three times using exponential backoff, with a five-second timeout on each attempt. InStore waits up to one second before the first retry, up to two seconds before the second retry, and up to four seconds before the third retry. Then, InStore stops retrying. This does not affect the finalized refund session. Your integration is responsible for reconciling any missed notifications through your own means. Each refund instruction in the [refund object](/instore/integration/refunds/delegated-refunds.md#refund-object) must specify its own `refundAmount`, `paymentOption`, and `originalCTPayment`. InStore does not calculate split refund amounts or select target payment options for you. ### Refund receipt data webhook To print a refund receipt, InStore requests data for a finalized refund session from your webhook URL. Unlike the finalize refund webhook, this is a synchronous request. InStore waits for your response and uses it to render the [refund receipt](/instore/implement-instore/receipts/refund-receipt.md). InStore signs this request the same way it signs [other webhook requests](/instore/implement-instore/custom-payments/payment-extensions.md#header-information-that-you-receive-in-all-instore-payment-requests). In addition to `X-Signature` and `X-Correlation-Id`, this request also includes the following headers: | Name | Purpose | | --- | --- | | `X-API-Key` | A key to authenticate the InStore API call to your webhook service. | | `x-tenant-id` | Identifies the tenant that is being called. | Because the response contains cart contents, InStore refuses to send this request at all if your tenant has no webhook verification key configured. Both blocks in `data` are stored records rather than assembled payloads, so some of their nested objects carry a storage `_id` of their own: `refunds[].refundAmount`, `tenderItems[].amount`, and a tender's `cash` block. Your integration doesn't need to read these. ```json title="Sample refund receipt data request payload" { "tenant": { "typeId": "tenant", "key": "acme-retail" }, "location": { "typeId": "location", "key": "01" }, "workstation": { "typeId": "workstation", "key": "001" }, "action": "RefundReceiptDataRetrieval", "data": { "refundSession": { "refundSessionId": "6a3c08d503353f78bbe2c08f", "returnCart": { "typeId": "cart", "key": "cart-1" }, "returnOrders": [ { "order": { "typeId": "order", "key": "order-1" }, "returnLineItem": [{ "typeId": "line-item", "id": "193eb21f-a5e7-4327-a6c9-705282da9f30" }] } ], "refunds": [ { "refundAmount": { "amount": 4999, "currency": "USD", "_id": "6a3c08d503353f78bbe2c08e" }, "paymentOption": { "typeId": "paymentOption", "key": "option-credit-us-1" }, "order": { "typeId": "order", "key": "order-1" }, "originalCTPayment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" }, "metaData": { "prompt": "Show1", "promptInfo": "Show2" }, "fallbackRefundTypes": [] } ], "status": "Refunded" }, "tenderItems": [ { "_id": "6a3c091003353f78bbe2c090", "session_id": "6a3c08d503353f78bbe2c08f", "transaction_type": "refund", "tender_type": "Credit", "amount": { "amount": 4999, "currency": "USD", "precision": 2, "_id": "6a3c091003353f78bbe2c093" }, "reference": "cart-1", "payment_id": "pay_3Nk82jLkQ4eR1mWx", "original_reference": "order-1", "payment_option_id": "option-credit-us-1", "report": { "tenderGroup": "Credit" }, "tenant_id": "acme-retail", "created_at": "2026-07-22T10:25:43.194Z", "__v": 0 } ] } } ``` | Field | Data type | Presence | Description | | --- | --- | --- | --- | | `tenant` / `location` / `workstation` | Object | Required | Scope references. These use `typeId`, not the `type` used by the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook). | | `action` | String | Required | Always `RefundReceiptDataRetrieval` for this request. | | `data.refundSession` | Object | Required | The refund session and its current `status`. It includes the return Cart, return Orders, and refunds that InStore built when the return was initiated. | | `data.refundSession.refundSessionId` | String | Required | The internal identifier of the InStore refund session. | | `data.refundSession.returnCart` | Object | Required | A reference to the Cart created for the returned items. Contains `typeId`, and whichever of `key` and `id` the refund object carried. InStore persists the reference as you sent it, and doesn't fill in the identifier you omitted. | | `data.refundSession.returnOrders` | Array | Required | The Orders and Line Items being returned, each with `order` (a reference) and `returnLineItem` (an array of references). | | `data.refundSession.refunds` | Array | Required | The refund instructions for the session, as persisted. Includes `refundAmount`, `paymentOption`, `order`, `originalCTPayment`, and `fallbackRefundTypes`, plus `transactionId`, `pspData`, and `metaData` when the refund object carried them. A refund instruction's own `customerId` isn't persisted at this level: read it from `fallbackRefundTypes[].customerId`, or take the account from `tenderInformation.customerId` on the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook). | | `data.refundSession.status` | String | Required | The refund session's current status, for example `Refunded`. | | `data.tenderItems` | Array | Required | Every tender recorded against the session, in the raw shape InStore persists it. Amounts use InStore tender money. | | `data.tenderItems[]._id` | String | Required | The tender identifier. Matches `tenderInformation.tenderItemId` in the [finalize refund webhook](/instore/integration/refunds/delegated-refunds.md#finalize-refund-webhook). | | `data.tenderItems[].session_id` | String | Required | The refund session the tender belongs to. | | `data.tenderItems[].transaction_type` | String | Required | Always `refund` for these tenders. | | `data.tenderItems[].tender_type` | String | Required | The tender type: `Cash`, `Credit`, `GiftCard`, `Wallet`, or `POA`. | | `data.tenderItems[].amount` | Object | Required | The tender amount: `{ amount, currency, precision }`. | | `data.tenderItems[].cash` | Object | Optional, `Cash` tenders only | The cash-rounding adjustment applied to the tender: `{ roundedRemainder: { amount, currency, precision } }`. | | `data.tenderItems[].reference` | String | Required | The return Cart this tender was taken against. | | `data.tenderItems[].original_reference` | String | Optional | The original Order the refund relates to. | | `data.tenderItems[].payment_id` | String | Optional | The original commercetools Payment being refunded. | | `data.tenderItems[].payment_option_id` | String | Optional | The payment option this tender was taken against. | | `data.tenderItems[].report.tenderGroup` | String | Optional | The reporting group frozen from the payment option when the tender was created. The X and Z reports group on this rather than on `tender_type`. Absent when the option resolved to no group; the report then counts the amount as Other. | | `data.tenderItems[].tenant_id` | String | Required | The tenant the tender belongs to. | | `data.tenderItems[].created_at` | String | Required | When the tender was recorded, in ISO 8601 format. | | `data.tenderItems[].__v` | Number | Required | An internal storage version key that your integration doesn't need to read. | Respond within five seconds with a `2xx` status code and the receipt data InStore uses to print the refund receipt. For the response fields and a sample, see [Refund receipt template](/instore/implement-instore/receipts/refund-receipt.md). If your webhook service returns a non-`2xx` status, times out, or is unreachable, InStore doesn't retry. The refund session is already finalized, so the return isn't affected. Instead, the InStore POS shows an empty receipt preview, and the store associate can still complete the refund without printing or emailing a receipt. ## Related pages - [Area overview page with navigation](/instore.md) - [Previous page: Refunds overview](/instore/integration/refunds/overview.md) - [Search documentation and API specs](/search.md)