Create an exchange
An exchange sends a unit back and gets another one in its place: another size or colour of the same product, or a different product. It is created with the same call as a return, POST /returns/create, by adding the wanted variant to the unit; the API works out the balance between the returned unit, the new one and the shipping, and, when the shopper owes money, issues a payment link. This page covers the three situations an integration meets: no amount due, an amount the shopper pays, and an amount the shopper gets back.
The first three steps of Create a return (read the order, read the policy, list the shipping methods) apply as they are; what follows assumes them. Every identifier below is the Shopify one, except the reason and the shipping method, which come from the API.
What you need from Shopify
| Value | Where it goes |
|---|---|
Order name (#1001) and the email of the customer of the order | POST /login |
| Line item id of the unit being exchanged | items[].id and the :id of /products/{id}/options and /search |
| Fulfillment id of that line | items[].fulfillmentId, or the value GET /orders gives for it |
| Product id and variant id the shopper wants | change.productId, change.variantId |
1. Check that the unit can be exchanged
In GET /orders, a unit is exchangeable when cannotBeExchanged is false, blacklisted is false, outOfDateExchanges is false and quantityAvailable is above zero, and it carries at least one reason of type Change. The Change reasons disappear from the item when the shop disabled exchanges for this shopper or the order already reached maxNumberOfExchangesPerOrder. A reason may carry productsChange: then the exchange is forced to those products.
GET /policies tells the kinds of exchange the shop offers: normalExchangesEnabled (the new unit ships when the shop decides, usually once the parcel is on its way or received) and instantExchangesEnabled (the new unit ships right away). differentProductExchangeEnabled says whether another product can be chosen. The price rules of the shop (whether a dearer or a cheaper variant is allowed, and whether the difference is charged or waived) are not in the policy: they are already applied to the catalog answers of the next step.
2. Find the variant the shopper wants
For another size or colour of the same product, ask the options of the line item with the reason chosen:
GET /products/14577068736858/options?reasonID=6726de5d-1f4c-4912-9050-3a7123dad020&lang=en{
"id": "8253231888",
"title": "Essentials slim T-shirt",
"options": [ { "name": "Size", "values": ["XS", "S", "M", "L"] } ],
"variants": [
{ "id": "47046763118885", "productId": "8253231888", "name": "Essentials slim T-shirt - XS", "description": "XS", "price": 29.95, "inventoryQuantity": 0, "selectedOptions": [ { "name": "Size", "value": "XS" } ] },
{ "id": "47046763118892", "productId": "8253231888", "name": "Essentials slim T-shirt - S", "description": "S", "price": 29.95, "inventoryQuantity": 12, "selectedOptions": [ { "name": "Size", "value": "S" } ] },
{ "id": "47046763118909", "productId": "8253231888", "name": "Essentials slim T-shirt - M", "description": "M", "price": 34.95, "inventoryQuantity": 3, "selectedOptions": [ { "name": "Size", "value": "M" } ] }
]
}For a different product, search the catalog of the shop from the same line item:
POST /products/14577068736858/search?lang=en
Content-Type: application/json
{ "queryText": "hoodie", "numResults": 5 }It answers recommendations, a list of products with the same shape, plus pageCursor to page (cursorAction after or before, up to fifteen results per page).
Two rules make these answers the source of truth:
- Offer only variants with
inventoryQuantityabove zero. The API sets it to zero for the variants the shop does not allow as a target (dearer or cheaper than the returned unit, per the policy), not only for those out of stock; in/searcha product with no allowed variant is left out, and second-life listings never appear. When the reason forces the target (productsChange), the options come priced at the returned unit and are not filtered. priceis what the shopper will be charged for the new unit, already adjusted by the shop rules: when the shop waives the difference of a size change, the variant shows the price of the returned unit.
Keep productId and id of the chosen variant.
3. Price the shipping
POST /carriers/methods takes the same items you are about to create, change included, so the shipping reflects the rules for exchanges (some shops offer free exchange shipments) and only the methods the shop priced for exchanges are listed; a quote asked without change returns the return prices and a method that later fails with 400 No valid prices for this method. When several methods come back, the shopper picks one (drop off, pick up, shop...). Keep id, shippingMethodId and shippingMethodType of the chosen method.
4. Create the exchange
POST /returns/create?lang=en
Content-Type: application/json
{
"items": [
{
"id": "14577068736858",
"fulfillmentId": "5294120468773",
"items": [
{
"fulfillmentId": "5294120468773",
"reasonId": "6726de5d-1f4c-4912-9050-3a7123dad020",
"change": { "productId": "8253231888", "variantId": "47046763118909", "variantTitle": "M" }
}
]
}
],
"methodId": "fdba82c7-abd2-441f-9498-15227cfa0514",
"shippingMethodId": "BNS_PCL",
"shippingType": "DropOff",
"paymentMethod": "CreditCard",
"instantExchange": false,
"termsAndConditionsAcceptance": true
}change.productIdandchange.variantIdare the Shopify identifiers of the new unit. Always sendvariantId: without it the API only resolves products with a single variant.reasonIdmust be a reason of typeChange.instantExchangemust match what the policy allows (400 Instant exchanges not allowed/Normal exchanges not allowed).paymentMethodis required in every creation: it is the method used when the balance leaves money for the shopper.- A return can mix returned and exchanged units; the balance is computed over the whole return.
The response and the balance
{
"id": "44891408138533",
"name": "1001-R1",
"state": "Pending",
"currency": "EUR",
"value": -5.00,
"shippingCost": 3.95,
"customerPaid": false,
"paymentURL": "https://your-shop.myshopify.com/12345/invoices/abcdef",
"paymentMethod": "CreditCard",
"needLabel": true,
"labelURL": null,
"qrCodeURL": null,
"includeInstantExchange": false,
"exchangeOrder": { "id": "1234567890", "name": "#D42", "isDraft": true },
"items": [
{
"id": "47046763118885",
"name": "Essentials slim T-shirt",
"price": 29.95,
"status": "OnHold",
"reason": { "id": "6726de5d-1f4c-4912-9050-3a7123dad020", "type": "Change" },
"changeItem": { "id": "47046763118909", "productId": "8253231888", "name": "Essentials slim T-shirt", "description": "M", "price": 34.95 }
}
]
}value is the balance of the units: positive when the shopper gets money back, negative when the new units cost more than the returned ones, zero when the prices match or the shop waives the difference. shippingCost is what the shopper pays for the return shipping (zero when free). The three situations:
Nothing to pay. value minus shippingCost is zero or above, so customerPaid is true and paymentURL is null. The label, the QR code and the tracking are generated right away; follow the return as described in Follow a return. Any positive balance is refunded by the shop when it processes the parcel, through paymentMethod.
The shopper owes money. When value minus shippingCost is below zero, customerPaid is false and paymentURL carries the invoice of a Shopify draft order for that amount (the difference plus the return shipping), created in the same call. Nothing else happens until it is paid: no label, no tracking, no email to the shopper (in the detail, canContinueWithoutCustomerPaid is false on Shopify). Show paymentURL to the shopper: Drelife does not send it. The link is also available afterwards at GET /returns/{id}/payment-url (404 when the return has no payment link). When the shopper pays, Shopify notifies Drelife, customerPaid becomes true and the label, the tracking and the exchange order follow within a few minutes; poll GET /returns/{id} for customerPaid, then for labelURL or qrCodeURL, with bounded retries.
The shopper gets money back. When the new unit is cheaper, value is positive: the difference (minus the return shipping, when the shopper pays it) is refunded through paymentMethod when the shop processes the parcel, like a return. StoreCredit adds the bonus of the policy (storeCreditExtraQuantity).
items[].changeItem echoes the new unit with the price actually applied; includeInstantExchange tells which kind of exchange was created (deprecated in the reference but still served).
The exchange order
Drelife creates the new order in Shopify once the return is paid (immediately when nothing is due, unless the shop delays the creation of exchanges to a later stage): a real order with the new units, tagged DawaOnHold (normal exchange) or DawaInstant (instant; no tag when the shop releases exchanges at creation), noted Exchange from Order 1001, with the attribute parentOrderName set to the original order; the original order gets an attribute named changeOrder- plus the suffix of the return (changeOrder-R1 for 1001-R1) with the name of the new one. A normal exchange is created with its fulfillment on hold and released at the moment the shop configured: when the request is validated, when the parcel is collected, in transit or delivered, or when the shop verifies or approves the return; an instant exchange ships at once. The return exposes it as exchangeOrder: id (the Shopify order id, or the draft order id while the shopper has not paid), name and isDraft; it is null until the order exists. The attributes and tags above let you find it in Shopify as well.
Pre-validation shops
When the policy says needValidation, the exchange is requested with POST /returns/request (units with change, no shipping or refund method) and the shop reviews it. Once accepted, the shopper completes it with POST /returns/{id}/validate (shipping, paymentMethod, decisions); if money is due at that point the payment link is produced asynchronously (read it from GET /returns/{id} or /payment-url a moment later), the shopper receives a payment email, and the label waits for the payment as above. The whole flow is in Pre-validation.
Errors specific to exchanges
| HTTP | errorCode | Message | Meaning |
|---|---|---|---|
| 400 | NotEnoughStock | NotEnoughStock | Unknown product or variant, variantId missing on a multi-variant product, a second-life listing, no stock, or a variant not allowed as a target (inventoryQuantity was zero); the body carries no more detail |
| 400 | BadRequest | Change variant ... for the same product ... is not eligible / ... for a different product ... is not eligible | The shop does not allow a dearer or cheaper target of that kind |
| 400 | BadRequest | Reason ... is not a change type / Reason ... requires a change product / Product ... is not valid | Wrong reason for an exchange, or a target outside productsChange |
| 400 | BadRequest | Instant exchanges not allowed / Normal exchanges not allowed / Exchanges are not allowed | Kind of exchange not offered by the shop, or exchanges blocked for this shopper |
| 400 | MaxNumberOfExchangesPerOrderExceeded | The order already has the maximum number of exchanges | |
| 400 | BadRequest | Invalid Payment Method | paymentMethod not in allowedPaymentMethods |
| 405 | CMSNotSupported | The shop platform cannot issue the payment of an exchange | |
| 405 | OrderLocked | Another creation for the order is in progress; retry in a few seconds |
The errors shared with returns (OutOfDate, NotReturnableItems, BlacklistedItems, quantities, shipping methods) are listed in Follow a return.
Updated about 2 months ago
