Returns & Cancellations
When a customer returns products, the platform automatically reverses the points earned and the cashback spent on those items. This page covers the full return flow with ticket examples, cancellation, product exchanges, edge cases, and rounding.
Original purchaseβ
Consider a customer with 459 available points who makes a purchase and redeems all available points as cashback:
POST /carts
{
"id": 1407113012324022967,
"currency": "EUR",
"total": 91.8,
"location_id": 12112221111,
"customer_id": 99000000000020,
"creation_date": "2024-11-12T13:05:33Z",
"lines": [
{
"order": 1,
"total": 16.8,
"quantity": 1,
"product_id": 77010380,
"original_total": 33.6
},
{
"order": 2,
"total": 75.0,
"quantity": 2,
"product_id": 2,
"original_total": 150.0
}
],
"cashback_score_desired": -1
}
Response (excerpt):
{
"id": 1407113012324022967,
"score": 184,
"score_as_money": 1.84,
"score_used": -459,
"score_used_as_money": -91.8,
"score_returned_to_customer": 0,
"score_returned_from_customer": 0,
"cashbacks": [
{ "line_order": 1, "score": 84, "discount": 16.8 },
{ "line_order": 2, "score": 375, "discount": 75.0 }
],
"scores": [
{ "score": 34, "line_order": 1, "loyalty_score_id": 555252320,
"available_from": "2024-11-13", "available_to": "2026-03-28" },
{ "score": 150, "line_order": 2, "loyalty_score_id": 555252320,
"available_from": "2024-11-13", "available_to": "2026-03-28" }
]
}
Summary of this purchase:
| Line 1 | Line 2 | Total | |
|---|---|---|---|
| Points earned | 34 | 150 | 184 |
| Cashback spent (pts) | 84 | 375 | 459 |
| Cashback spent (β¬) | β¬16.80 | β¬75.00 | β¬91.80 |
Partial returnβ
The customer wants to return line 1 (1 unit) and 1 unit from line 2 (which had 2 units). A new ticket is sent referencing the original:
POST /carts
{
"id": 1407113012324022968,
"currency": "EUR",
"total": -54.3,
"location_id": 12112221111,
"customer_id": 99000000000020,
"creation_date": "2024-11-12T17:05:33Z",
"lines": [
{
"order": 1,
"total": -16.8,
"quantity": -1,
"product_id": 77010380,
"original_total": -16.8
},
{
"order": 2,
"total": -37.5,
"quantity": -1,
"product_id": 2,
"original_total": 150.0
}
],
"returns": [
{
"ticket_id": 1407113012324022967,
"quantity": 1,
"line_order": 1,
"from_line_order": 1
},
{
"ticket_id": 1407113012324022967,
"quantity": 1,
"line_order": 2,
"from_line_order": 2
}
]
}
returns[] field reference:
| Field | Description |
|---|---|
ticket_id | ID of the original purchase ticket |
quantity | Number of units being returned (positive integer) |
line_order | Line number in the original ticket |
from_line_order | Line number in the current return ticket |
Cart lines in a return must have negative total and quantity. If a wrong ticket_id is sent in returns[], the API returns an error.
What the engine reverses per line:
| Line 1 | Line 2 (1 of 2 units) | |
|---|---|---|
| Points subtracted (earned β taken back) | 34 pts | 75 pts (150 Γ· 2) |
| Cashback returned to customer (spent β given back) | 84 pts | 188 pts (375 Γ· 2) |
Response fields for score movements:
| Field | Meaning |
|---|---|
score_returned_from_customer | Points deducted from customer because they had been earned on the returned items |
score_returned_to_customer | Points restored to customer because they had been spent as cashback on the returned items |
Full ticket cancellationβ
To cancel an entire purchase in a single atomic operation, use a dedicated endpoint β do not send a return cart with all lines negative:
POST /carts/cancellation
{
"cart_to_cancel_id": 1407113012324022967
}
Alternatively, identify the original cart by code:
POST /carts/cancellation
{
"cart_to_cancel_code": "POS-REF-00123"
}
What the engine reverses atomically:
| Effect reversed | Description |
|---|---|
| Points earned | All accrued score is deducted from the customer's balance |
| Cashback spent | All points redeemed as cashback are restored to the customer's wallet |
| Discounts applied | All promotional line modifications are rolled back |
| Vouchers / coupons used | All voucher redemptions tied to the original ticket are reversed β the voucher returns to available status and can be re-used |
A cart can only be cancelled once. If cart_to_cancel_id refers to a cart already cancelled or returned, the API returns an error.
Product exchange (no score impact)β
When a customer returns a product and immediately receives a replacement in the same transaction (different size, colour, etc.) with no net monetary change, the operation uses the same cancellation endpoint with an additional new_cart_id field:
POST /carts/cancellation
{
"cart_to_cancel_id": 1407113012324022967,
"new_cart_id": 1407113012324022999
}
The engine cancels the original cart (reversing its loyalty effects) and links the replacement cart as the authoritative new purchase. Points and cashback are recalculated on the new cart only β the exchange is a single atomic swap from the loyalty program's perspective.
When using codes instead of IDs:
POST /carts/cancellation
{
"cart_to_cancel_code": "POS-REF-00123",
"new_cart_code": "POS-REF-00124"
}
Return edge casesβ
Cashback spent on the returned purchaseβ
When a customer used cashback on the original purchase and then returns items, the platform restores the spent points to the customer's score_available:
- Restored balance carries the same expiry dates as the original balance that was used.
- If any portion of the original balance had already expired between the purchase date and the return date, only the non-expired portion is restored. Expired points are not recovered.
Earned points already spent before the returnβ
When a customer earns points on purchase A, spends them on purchase B, and then returns purchase A:
- The engine must deduct the points originally earned on purchase A.
- Since those points are already spent, the customer's balance goes negative.
- Compensation: any future points earned are first applied to offset the negative balance before they become spendable.
Example:
| Event | Points balance | Available |
|---|---|---|
| Buy Product A β earn 200 pts | 200 | 200 |
| Use 200 pts as cashback on purchase B | 0 | 0 |
| Return Product A β subtract 200 pts | β200 | β200 |
| Next purchase β earn 300 pts | 100 | 100 |
The 300 newly earned points first compensate the β200 deficit, leaving 100 spendable.
Direct value campaigns on partial returnβ
The reversal behaviour depends on the campaign's distribution type:
| Campaign effect | Partial return behaviour |
|---|---|
direct_value with distribution: "total" | Points are not reversed on partial return β only reversed on full cancellation (POST /carts/cancellation) |
Any effect with distribution: "individual_items" | Points reversed proportionally to the returned lines |
ratio or percentage | Points reversed proportionally to the returned amount |
Roundingβ
Points awarded at earn time are rounded to the nearest integer β see Earning rules β Rounding for the full explanation.
Partial return amountsβ
When calculating the amount to reverse on a partial return, the engine uses banker's rounding (round-half-to-even) to avoid systematic bias across many small returns:
return_proportion = returned_quantity / original_quantity
reversed_value = original_value Γ (-return_proportion) [RoundBank to ValuePrecision]
Example: original effect value = β¬37.50 on 2 units, returning 1 unit:
return_proportion = 1 / 2 = 0.5
reversed_value = 37.50 Γ -0.5 = -18.75
No rounding difference here, but for amounts like β¬0.005 banker's rounding ensures consistency.