Understand how InStore delegates returns and refunds to your integration, and implement the webhook notifications your integration receives.
Set up 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 transaction on your behalf. Your payment processor integration remains responsible for reversing the original charge.
Cash tenders are the exception. For the processor endpoint and payload, see
Refund webhook.
InStore provides for delegation of these refund types:
Cash tenders 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:
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, which your host application sends to InStore. |
| InStore tender money | { amount, currency, precision } | data.tenderItems[].amount in the receipt data request, 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 is the sender, but the shape matches the REST API 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 or 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 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, the return
Orders and
Line Items, and one or more refund instructions. Each refund instruction names a payment option and the original commercetools
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.
Sample refund objectjson
{
"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 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 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. |
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 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 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 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.
The refund itself goes to your refund path, and the tender is recorded as Credit.
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:
Sample pay-on-account refund instructionjson
{
"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:
- 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 carries the field.
- Stores the first
customerId it finds across the refunds and their fallbacks on the refund session.
- Reports that stored value back to you as
tenderInformation.customerId in the 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, InStore adds a
Refund 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 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 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 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.
POST {processorUrl}/refund
Sample refund requestjson
{
"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 being refunded. |
amount | Object | Required | The refund amount, as commercetools money. Positive. |
paymentMethod | Object | Required | How the refund is processed. See 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. For the two response shapes, see
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. |
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 to its
callbackUrl.
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.
Sample progress messagejson
{
"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. |
data.title, data.message, data.options, data.primary | Various | Optional | For prompt. See Prompts. |
data.inputType, data.inputData, data.callbackUrl | Various | Optional | For input. See 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.
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.
Sample promptjson
{
"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:
POST {processorUrl}/resolve
Sample resolve requestjson
{
"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 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.
Result events
Post exactly one result per refund attempt. InStore stops accepting events for the attempt as soon as one arrives.
Sample successful refundjson
{
"event": "Refunded",
"data": {
"payment": { "typeId": "payment", "id": "pay_3Nk82jLkQ4eR1mWx" },
"transactionId": "0f6d6b4f-9b3f-4f26-9f0a-2a0f6b2b8a11",
"label": "Refund",
"amount": { "currencyCode": "USD", "centAmount": 4999, "fractionDigits": 2 }
}
}
Sample declined refundjson
{
"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. 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. 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.
Sample finalize refund webhook payloadjson
{
"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. |
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 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 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. 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 or
Transaction. Your payment processor integration remains responsible for the reversal. For a cash tender, InStore adds the refund transaction to the commercetools
Payment before sending this webhook. For more information, see
Credit,
Stored value,
Wallet, and
Pay-on-account 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 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 signs this request the same way it signs
other webhook 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.
Sample refund receipt data request payloadjson
{
"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. |
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. |
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. |
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.
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.