Contents
Returns Portal API
Let shoppers find their order and create a return, exchange or store-credit request.
The public API behind the Nucleo-hosted returns page (https://app.nucleoplatform.com/returns/{slug}).
Use it to build your own returns experience inside your storefront or app: identify the order with
its number plus the shopper's email or postcode, show what can be returned and how, create the return
(refund, exchange with live stock, or store credit; carrier label or in-store drop-off; up to three
photos), then follow it on a status page and download the return label.
There are no API keys. The shopper proves ownership with order number + email or postcode and gets a
short-lived encrypted session token (X-Return-Session, one hour, one order). Each created return
gets a long random return token that opens its status page and label without any session.
Every rule applied here (return window, non-returnable items, reasons, fees, exchange options) is the merchant's return policy, the same one used by customer service and the warehouse.
Authentication
returnSessionAPI key · header · X-Return-SessionSession token returned by
POST /lookup(data.session). Encrypted and signed by Nucleo, bound to the store and one order, valid 60 minutes; not renewable — look the order up again when it expires.
- Base URL
- https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/{slug}Production
- Who calls it
- Storefronts
- Endpoints
- 6
- OpenAPI 3.1 specification
- returns.yaml
{slug} — The store's returns-portal slug (^[a-z0-9][a-z0-9-]{0,63}$). It is the last segment of the
portal link shown in Settings › Orders › Returns and refunds › Return portal
(https://app.nucleoplatform.com/returns/{slug}).
(examples use acme; full URL https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme)Portal
Branding and rules shown before the shopper signs in.
Turning it on. In Nucleo go to Settings › Orders › Returns and refunds › Return portal,
switch the portal on and copy its link; the Return policy tab sets window, reasons, resolutions,
fees and exclusions. While the portal is off, every endpoint that needs it answers 404 as if the
store did not exist (return status pages and labels keep working).
/Get portal branding and rules
Brand (name, logo, accent colour, support contacts), languages, how the shopper can identify the
order (verify_with), the default return window, the return fee and which resolutions are offered.
Use it to render the sign-in step.
Rate limit: 60 requests per minute per client (bucket shared with /order, /r/{token} and
/r/{token}/label), and 1,800 per minute for the whole store.
Responses
200Portal information.application/json
dataPortalInforequiredChild attributes
brandBrandrequiredChild attributes
namestringrequiredExample:Acme Apparellogo_urlstring (uri) | nullrequiredaccent_colorstring | nullrequiredExample:#0F766Esupport_emailstring (email) | nullrequiredsupport_urlstring (uri) | nullrequiredslugstringrequiredExample:acme
languagesarray of stringrequiredOne ofitendefresdefault_languagestringrequiredOne ofitendefresverify_witharray of stringrequiredWhat the shopper may give besides the order number.
One ofemailzipwindow_daysintegerrequiredDefault return window in days from delivery (per-country/channel windows may differ; see
order.returnable_until).Example:30feenumberrequiredReturn fee withheld from refunds, in the order currency (0 = free).
Example:4.9resolutionsResolutionsrequiredWhich outcomes the shopper may choose.
Child attributes
refundbooleanrequiredexchangebooleanrequiredstore_creditbooleanrequired
404The store does not exist or its returns portal is switched off. The body is an empty message. A slug that does not match
^[a-z0-9][a-z0-9-]{0,63}$gets the generic route-not-found message. application/jsonmessagestringrequired
429Rate limit exceeded. No
Retry-Afterheader; waitretry_afterseconds.application/jsonmessagestringrequiredOne oftoo_many_requeststoo_many_attemptsretry_afterintegerrequiredSeconds until the limit resets.
curl -X GET https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/ \
-H 'Accept: application/json'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/", {
method: "GET",
headers: {
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/', [
'headers' => [
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"brand": {
"name": "Acme Apparel",
"logo_url": "https://cdn.acme.example/logo.svg",
"accent_color": "#0F766E",
"support_email": "help@example.com",
"support_url": "https://shop.acme.example/pages/contact",
"slug": "acme"
},
"languages": [
"it",
"en",
"de",
"fr",
"es"
],
"default_language": "en",
"verify_with": [
"email",
"zip"
],
"window_days": 30,
"fee": 4.9,
"resolutions": {
"refund": true,
"exchange": true,
"store_credit": true
}
}
}{
"message": ""
}{
"message": "The route api/oms/v1/public/returns/Acme_Store could not be found."
}{
"message": "too_many_requests",
"retry_after": 42
}Order lookup
Identify the order and open a one-hour session.
Building your own UI. The API can be called from the browser (CORS allows any origin and the
X-Return-Session header) or from your server. Rate limits are counted per caller IP and the first
address in X-Forwarded-For: if you proxy calls through your own server, forward the shopper's IP in
X-Forwarded-For, otherwise all your shoppers share one bucket. Responses for "order not found" and
"wrong email/postcode" are identical on purpose.
/lookupFind an order and open a session
Finds the order by number (with or without the leading #, case-insensitive) and checks the
verifier against the order's email (case-insensitive) or shipping postcode (spaces and hyphens
ignored), as allowed by the policy's verify_with. Exchange orders created by Nucleo cannot be
looked up.
On success returns a session token valid for one hour for this order only, the resolved language and the full order view (what can be returned, reasons, resolutions, methods, fees, previous returns).
Language is the first of: language in the body, the order's language, the policy default —
each only if enabled in the policy — otherwise en.
Rate limits
- 10 lookups per minute per client (300 per minute for the whole store);
- 5 failed attempts per order number every 15 minutes: after that the order number is locked
(
429 too_many_attempts) until the window expires. A successful lookup resets the counter.
Validation errors (422) do not count towards the limits.
Request bodyapplication/json
orderstringrequiredOrder number, with or without
#.max length 40Example:#1042verifierstringrequiredThe email used for the order or the shipping postcode (whichever the policy allows).
max length 120Example:giulia.rossi@example.comlanguagestring | nullPreferred language (
it,en,de,fr,es); ignored if not enabled.max length 5Example:it
Responses
200Order found; session opened.application/json
dataobject & OrderViewrequiredChild attributes
sessionstringrequiredOpaque session token. Send it as
X-Return-Sessionto/orderand/returns.expires_inintegerrequiredSeconds until the session expires (always 3600).
Example:3600languagestringrequiredOne ofitendefresorderobjectrequiredChild attributes
namestringrequiredExample:#1042placed_atstring (date-time) | nullrequireddelivered_atstring (date-time) | nullrequiredcurrencystringrequiredExample:EURcountrystring | nullrequiredShipping country.
Example:ITreturnable_untilstring (date-time) | nullrequiredEnd of the return window (from delivery, or shipment + 3 days if delivery is unknown).
blockedstring | nullrequiredWhy the whole order cannot be returned (
null= it can):cancelled,not_shipped,window_closed,limit_reached(the customer reached the policy's maximum returns per period).One ofcancellednot_shippedwindow_closedlimit_reachednulllanguagestring | nullrequiredLanguage the order was placed in.
linesarray of OrderLinerequiredAttributes of each item
idstring (uuid)requiredOrder line ID, to reference in
lines[].idwhen creating the return.titlestringrequiredExample:Organic Cotton Teesizestring | nullrequiredParsed from SKUs shaped
MODEL_COLOR_SIZE;nullotherwise.Example:Mcolorstring | nullrequiredExample:BLKskustring | nullrequiredExample:TEE_BLK_Mimage_urlstring (uri) | nullrequiredProduct image from the Nucleo Catalog.
pricenumberrequiredUnit price paid, tax included.
Example:39quantityintegerrequiredQuantity ordered.
returnableintegerrequiredQuantity that can still be returned (shipped minus already in non-cancelled returns).
blockedstring | nullrequiredWhy this line cannot be returned (
null= it can). Either the order-level reason, orexcluded_sku(non-returnable product),excluded_tag(order tagged as non-returnable),final_sale(discount above the policy threshold),not_shipped,already_returned.One ofcancellednot_shippedwindow_closedlimit_reachedexcluded_skuexcluded_tagfinal_salealready_returnednullexchange_optionsarray of objectrequiredOther sizes (and colours, if the policy allows) of the same model, same colour first. Empty when the line is blocked, exchanges are off, or the SKU is not shaped
MODEL_COLOR_SIZE.Attributes of each item
barcodestringrequiredPass as
exchange_barcode.Example:8001234567892sizestring | nullrequiredExample:Lcolorstring | nullrequiredExample:BLKsame_colorbooleanrequiredpricenumberrequiredCurrent price of the replacement.
in_stockbooleanrequiredimage_urlstring (uri) | nullrequired
reasonsarray of objectrequiredActive return reasons, labelled in the session language.
Attributes of each item
codestringrequiredStable code (defaults:
too_small,too_big,style,color,defective,wrong_item,not_as_described,changed_mind,other; merchants can add their own).Example:too_smalllabelstringrequiredExample:Too smallphotostringrequiredWhether a photo is asked for when this reason is chosen.
One ofnoneoptionalrequired
resolutionsResolutionsrequiredWhich outcomes the shopper may choose.
Child attributes
refundbooleanrequiredexchangebooleanrequiredstore_creditbooleanrequired
store_credit_bonus_pctnumberrequiredExtra percentage granted when choosing store credit instead of a refund.
Example:10methodsobjectrequiredChild attributes
carrier_labelbooleanrequiredA prepaid carrier label can be generated.
store_dropoffbooleanrequiredThe items can be dropped off in one of
stores.storesarray of objectrequiredDrop-off stores, those in the order's shipping country first.
Attributes of each item
idstring (uuid)requiredPass as
store_idwhen creating astore_dropoffreturn.namestringrequiredcountrystring | nullrequiredaddressstring | nullrequired
feeobjectrequiredChild attributes
amountnumberrequiredReturn fee withheld from the refund.
free_for_exchangebooleanrequiredfree_for_store_creditbooleanrequiredfree_in_storebooleanrequiredfree_reasonsarray of stringrequiredReason codes for which the return is free.
exchangeobjectrequiredChild attributes
upchargestringrequiredIf the replacement costs more, the shopper pays the difference (
invoice) or the merchant absorbs it up toabsorb_max.One ofinvoiceabsorbabsorb_maxnumberrequireddownchargestringrequiredIf the replacement costs less, how the difference goes back to the shopper.
One ofrefundstore_credit
returnsarray of objectrequiredPrevious returns of this order (cancelled ones excluded), newest first.
Attributes of each item
namestringrequiredExample:#1042-R1statusstringrequiredThe return in the shopper's words:
requested(created, not yet on its way),in_transit,received,checking(received, refund under manual review),completed,refunded,cancelled.One ofrequestedin_transitreceivedcheckingcompletedrefundedcancelledtokenstring | nullrequiredReturn token for the status page.
created_atstring (date-time) | nullrequired
404
not_found: no such order, or the email/postcode does not match (indistinguishable on purpose). Also returned, with an emptymessage, when the store or its portal does not exist or is off. application/jsonmessagestringrequired
422Invalid request body.application/json
messagestringrequirederrorsobjectrequired
429Too many lookups from this client, or too many failed attempts on this order number.application/json
messagestringrequiredOne oftoo_many_requeststoo_many_attemptsretry_afterintegerrequiredSeconds until the limit resets.
curl -X POST https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/lookup \
-H 'Content-Type: application/json' \
-d '{
"order": "#1042",
"verifier": "giulia.rossi@example.com",
"language": "it"
}'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/lookup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"order": "#1042",
"verifier": "giulia.rossi@example.com",
"language": "it"
}),
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/lookup', [
'json' => [
'order' => '#1042',
'verifier' => 'giulia.rossi@example.com',
'language' => 'it',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"session": "eyJpdiI6IkxQb3Z6c2J0c0Z6V2Z6dz09IiwidmFsdWUiOiJ3c3l6Q2R0Wm1rRk9TVnlqK2c9PSIsIm1hYyI6IjhhM2YifQ==",
"expires_in": 3600,
"language": "it",
"order": {
"name": "#1042",
"placed_at": "2026-09-20T10:12:00+00:00",
"delivered_at": "2026-09-23T14:30:00+00:00",
"currency": "EUR",
"country": "IT",
"returnable_until": "2026-10-23T23:59:59+00:00",
"blocked": null,
"language": "it"
},
"lines": [
{
"id": "9d3f6b1e-2a4c-4e8b-a1f0-6c2d8e4b7a10",
"title": "Organic Cotton Tee",
"size": "M",
"color": "BLK",
"sku": "TEE_BLK_M",
"image_url": "https://cdn.acme.example/products/tee-blk.jpg",
"price": 39,
"quantity": 1,
"returnable": 1,
"blocked": null,
"exchange_options": [
{
"barcode": "8001234567892",
"size": "L",
"color": "BLK",
"same_color": true,
"price": 39,
"in_stock": true,
"image_url": "https://cdn.acme.example/products/tee-blk.jpg"
},
{
"barcode": "8001234567908",
"size": "M",
"color": "WHT",
"same_color": false,
"price": 39,
"in_stock": false,
"image_url": "https://cdn.acme.example/products/tee-wht.jpg"
}
]
}
],
"reasons": [
{
"code": "too_small",
"label": "Troppo piccolo",
"photo": "none"
},
{
"code": "defective",
"label": "Difettoso o danneggiato",
"photo": "required"
}
],
"resolutions": {
"refund": true,
"exchange": true,
"store_credit": true
},
"store_credit_bonus_pct": 10,
"methods": {
"carrier_label": true,
"store_dropoff": true,
"stores": [
{
"id": "4b8e2c71-0f3a-4d59-9e6b-2a7c1d8f5e03",
"name": "Acme Apparel Milano",
"country": "IT",
"address": "Via Roma 1, 20121 Milano"
}
]
},
"fee": {
"amount": 4.9,
"free_for_exchange": true,
"free_for_store_credit": true,
"free_in_store": true,
"free_reasons": [
"defective",
"wrong_item"
]
},
"exchange": {
"upcharge": "invoice",
"absorb_max": 0,
"downcharge": "refund"
},
"returns": []
}
}{
"message": "not_found"
}{
"message": "The order field is required.",
"errors": {
"order": [
"The order field is required."
]
}
}{
"message": "too_many_requests",
"retry_after": 42
}{
"message": "too_many_attempts",
"retry_after": 873
}/orderGet the order of the current session
The same order view returned by /lookup, recomputed now (returnable quantities, exchange stock,
previous returns), without opening a new session. Use it to refresh the form or switch language.
Rate limit: 60 requests per minute per client (shared portal bucket), 1,800 per minute per store.
X-Return-Session headerQuery parameters
languagestringPreferred language; used only if enabled in the policy (see
/lookup).One ofitendefresExample:it
Responses
200The order view.application/json
dataobject & OrderViewrequiredChild attributes
languagestringrequiredOne ofitendefresorderobjectrequiredChild attributes
namestringrequiredExample:#1042placed_atstring (date-time) | nullrequireddelivered_atstring (date-time) | nullrequiredcurrencystringrequiredExample:EURcountrystring | nullrequiredShipping country.
Example:ITreturnable_untilstring (date-time) | nullrequiredEnd of the return window (from delivery, or shipment + 3 days if delivery is unknown).
blockedstring | nullrequiredWhy the whole order cannot be returned (
null= it can):cancelled,not_shipped,window_closed,limit_reached(the customer reached the policy's maximum returns per period).One ofcancellednot_shippedwindow_closedlimit_reachednulllanguagestring | nullrequiredLanguage the order was placed in.
linesarray of OrderLinerequiredAttributes of each item
idstring (uuid)requiredOrder line ID, to reference in
lines[].idwhen creating the return.titlestringrequiredExample:Organic Cotton Teesizestring | nullrequiredParsed from SKUs shaped
MODEL_COLOR_SIZE;nullotherwise.Example:Mcolorstring | nullrequiredExample:BLKskustring | nullrequiredExample:TEE_BLK_Mimage_urlstring (uri) | nullrequiredProduct image from the Nucleo Catalog.
pricenumberrequiredUnit price paid, tax included.
Example:39quantityintegerrequiredQuantity ordered.
returnableintegerrequiredQuantity that can still be returned (shipped minus already in non-cancelled returns).
blockedstring | nullrequiredWhy this line cannot be returned (
null= it can). Either the order-level reason, orexcluded_sku(non-returnable product),excluded_tag(order tagged as non-returnable),final_sale(discount above the policy threshold),not_shipped,already_returned.One ofcancellednot_shippedwindow_closedlimit_reachedexcluded_skuexcluded_tagfinal_salealready_returnednullexchange_optionsarray of objectrequiredOther sizes (and colours, if the policy allows) of the same model, same colour first. Empty when the line is blocked, exchanges are off, or the SKU is not shaped
MODEL_COLOR_SIZE.Attributes of each item
barcodestringrequiredPass as
exchange_barcode.Example:8001234567892sizestring | nullrequiredExample:Lcolorstring | nullrequiredExample:BLKsame_colorbooleanrequiredpricenumberrequiredCurrent price of the replacement.
in_stockbooleanrequiredimage_urlstring (uri) | nullrequired
reasonsarray of objectrequiredActive return reasons, labelled in the session language.
Attributes of each item
codestringrequiredStable code (defaults:
too_small,too_big,style,color,defective,wrong_item,not_as_described,changed_mind,other; merchants can add their own).Example:too_smalllabelstringrequiredExample:Too smallphotostringrequiredWhether a photo is asked for when this reason is chosen.
One ofnoneoptionalrequired
resolutionsResolutionsrequiredWhich outcomes the shopper may choose.
Child attributes
refundbooleanrequiredexchangebooleanrequiredstore_creditbooleanrequired
store_credit_bonus_pctnumberrequiredExtra percentage granted when choosing store credit instead of a refund.
Example:10methodsobjectrequiredChild attributes
carrier_labelbooleanrequiredA prepaid carrier label can be generated.
store_dropoffbooleanrequiredThe items can be dropped off in one of
stores.storesarray of objectrequiredDrop-off stores, those in the order's shipping country first.
Attributes of each item
idstring (uuid)requiredPass as
store_idwhen creating astore_dropoffreturn.namestringrequiredcountrystring | nullrequiredaddressstring | nullrequired
feeobjectrequiredChild attributes
amountnumberrequiredReturn fee withheld from the refund.
free_for_exchangebooleanrequiredfree_for_store_creditbooleanrequiredfree_in_storebooleanrequiredfree_reasonsarray of stringrequiredReason codes for which the return is free.
exchangeobjectrequiredChild attributes
upchargestringrequiredIf the replacement costs more, the shopper pays the difference (
invoice) or the merchant absorbs it up toabsorb_max.One ofinvoiceabsorbabsorb_maxnumberrequireddownchargestringrequiredIf the replacement costs less, how the difference goes back to the shopper.
One ofrefundstore_credit
returnsarray of objectrequiredPrevious returns of this order (cancelled ones excluded), newest first.
Attributes of each item
namestringrequiredExample:#1042-R1statusstringrequiredThe return in the shopper's words:
requested(created, not yet on its way),in_transit,received,checking(received, refund under manual review),completed,refunded,cancelled.One ofrequestedin_transitreceivedcheckingcompletedrefundedcancelledtokenstring | nullrequiredReturn token for the status page.
created_atstring (date-time) | nullrequired
401
session_expired: missing, invalid or expiredX-Return-Session, or a session of another store.application/jsonmessagestringrequired
404The store does not exist or its returns portal is switched off. The body is an empty message. A slug that does not match
^[a-z0-9][a-z0-9-]{0,63}$gets the generic route-not-found message. application/jsonmessagestringrequired
429Rate limit exceeded. No
Retry-Afterheader; waitretry_afterseconds.application/jsonmessagestringrequiredOne oftoo_many_requeststoo_many_attemptsretry_afterintegerrequiredSeconds until the limit resets.
curl -X GET 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/order?language=it' \
-H "X-Return-Session: $NUCLEO_RETURN_SESSION" \
-H 'Accept: application/json'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/order?language=it", {
method: "GET",
headers: {
"X-Return-Session": `${process.env.NUCLEO_RETURN_SESSION}`,
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/order?language=it', [
'headers' => [
'X-Return-Session' => getenv('NUCLEO_RETURN_SESSION'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"language": "it",
"order": {
"name": "#1042",
"placed_at": "2026-10-01T09:30:00Z",
"delivered_at": "2026-10-01T09:30:00Z",
"currency": "EUR",
"country": "IT",
"returnable_until": "2026-10-01T09:30:00Z",
"blocked": "cancelled",
"language": "string"
},
"lines": [
{
"id": "9b2f4c1e-5d7a-4e8b-9c3d-2a1f0e6b7c8d",
"title": "Organic Cotton Tee",
"size": "M",
"color": "BLK",
"sku": "TEE_BLK_M",
"image_url": "https://shop.acme.example",
"price": 39,
"quantity": 1,
"returnable": 1,
"blocked": "cancelled",
"exchange_options": [
{
"barcode": "8001234567892",
"size": "L",
"color": "BLK",
"same_color": true,
"price": 1.5,
"in_stock": true,
"image_url": "https://shop.acme.example"
}
]
}
],
"reasons": [
{
"code": "too_small",
"label": "Too small",
"photo": "none"
}
],
"resolutions": {
"refund": true,
"exchange": true,
"store_credit": true
},
"store_credit_bonus_pct": 10,
"methods": {
"carrier_label": true,
"store_dropoff": true,
"stores": [
{
"id": "9b2f4c1e-5d7a-4e8b-9c3d-2a1f0e6b7c8d",
"name": "string",
"country": "string",
"address": "string"
}
]
},
"fee": {
"amount": 1.5,
"free_for_exchange": true,
"free_for_store_credit": true,
"free_in_store": true,
"free_reasons": [
"string"
]
},
"exchange": {
"upcharge": "invoice",
"absorb_max": 1.5,
"downcharge": "refund"
},
"returns": [
{
"name": "#1042-R1",
"status": "requested",
"token": "string",
"created_at": "2026-10-01T09:30:00Z"
}
]
}
}{
"message": "session_expired"
}{
"message": ""
}{
"message": "The route api/oms/v1/public/returns/Acme_Store could not be found."
}{
"message": "too_many_requests",
"retry_after": 42
}Returns
Create a return for the order of the current session.
/returnsCreate a return
Creates a return for the session's order. Send either JSON or multipart/form-data (needed for
photos). The return itself goes in payload: a JSON object (JSON body) or a JSON-encoded string
(multipart). A JSON body without the payload wrapper is accepted too.
Checks, in order (the first failure is returned as 422 with a code in message):
- the order as a whole can be returned (
cancelled,not_shipped,window_closed,limit_reached); methodis offered (invalid_method) and, forstore_dropoff,store_idis one of the listed stores (invalid_store);- for each line: still returnable in that quantity (
line_not_returnable),reasonis an active reason (invalid_reason),resolutionis enabled (invalid_resolution), and forexchangethe chosenexchange_barcodeis among the line's options and in stock for the quantity (exchange_unavailable); - at least one valid line (
no_lines) — lines with an unknownidor a quantity of 0 are skipped; - at least one photo when a chosen reason requires it (
photo_required).
What happens
- A return
{order}-R{n}is created in statusrequested, with the return fee computed by the policy (free for exchange/store credit, in-store drop-off or "our fault" reasons if so configured). - The return is routed to the location that will receive it.
- Exchanges: a replacement order
{order}-EX{n}is created and the new size/colour is reserved. In standard mode it ships when the return arrives intact; in advance mode (if the policy and the customer's history allow it) it ships straight away. If the replacement costs more and the policy says so, the shopper is asked to pay the difference before it ships. carrier_label: a prepaid return label is generated with the merchant's return carrier. If label creation fails the return is still created (has_label: falseon the status page).store_dropoff: a drop-off code and QR code are issued for the store.
Not idempotent: each successful call creates a new return.
Rate limit: 10 requests per minute per client, 300 per minute per store.
X-Return-Session headerRequest bodyapplication/json, multipart/form-data
payloadCreateReturnrequiredChild attributes
methodstringrequiredMust be offered in the order view's
methods.One ofcarrier_labelstore_dropoffstore_idstring (uuid)Required for
store_dropoff; one ofmethods.stores[].id.languagestringOne ofitendefresnotestringFree note for the merchant (trimmed, truncated at 2,000 characters).
linesarray of objectrequiredmin items 1Attributes of each item
idstring (uuid)requiredOrder line ID from
lines[].id.quantityintegerrequiredAt most the line's
returnable.min 1reasonstringrequiredAn active reason
code.Example:too_smallresolutionstringOne ofrefundexchangestore_creditdefault refundexchange_barcodestringRequired for
exchange; one of the line'sexchange_options[].barcode, in stock.commentstringFree comment on the line (trimmed, truncated at 1,000 characters).
Responses
201Return created.application/json
dataobjectrequiredChild attributes
tokenstringrequiredReturn token for
/r/{token}and/r/{token}/label. Keep it; it is the only handle.pattern ^[A-Za-z0-9]{40}$namestringrequiredReturn number shown to the shopper.
401
session_expired: missing, invalid or expiredX-Return-Session, or a session of another store.application/jsonmessagestringrequired
404The store does not exist or its returns portal is switched off. The body is an empty message. A slug that does not match
^[a-z0-9][a-z0-9-]{0,63}$gets the generic route-not-found message. application/jsonmessagestringrequired
422Business rule failure (
messageis a code, see the description) or invalid photos (standard validation body witherrors).invalid_payloadwhenpayloadis not a JSON object. application/jsonOne of the following shapes:
Message
messagestringrequired
ValidationError
messagestringrequirederrorsobjectrequired
429Rate limit exceeded. No
Retry-Afterheader; waitretry_afterseconds.application/jsonmessagestringrequiredOne oftoo_many_requeststoo_many_attemptsretry_afterintegerrequiredSeconds until the limit resets.
curl -X POST https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/returns \
-H "X-Return-Session: $NUCLEO_RETURN_SESSION" \
-H 'Content-Type: application/json' \
-d '{
"payload": {
"method": "carrier_label",
"language": "it",
"lines": [
{
"id": "9d3f6b1e-2a4c-4e8b-a1f0-6c2d8e4b7a10",
"quantity": 1,
"reason": "too_small",
"resolution": "refund",
"comment": "Runs a bit tight on the shoulders"
}
]
}
}'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/returns", {
method: "POST",
headers: {
"X-Return-Session": `${process.env.NUCLEO_RETURN_SESSION}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"payload": {
"method": "carrier_label",
"language": "it",
"lines": [
{
"id": "9d3f6b1e-2a4c-4e8b-a1f0-6c2d8e4b7a10",
"quantity": 1,
"reason": "too_small",
"resolution": "refund",
"comment": "Runs a bit tight on the shoulders"
}
]
}
}),
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/returns', [
'headers' => [
'X-Return-Session' => getenv('NUCLEO_RETURN_SESSION'),
],
'json' => [
'payload' => [
'method' => 'carrier_label',
'language' => 'it',
'lines' => [
[
'id' => '9d3f6b1e-2a4c-4e8b-a1f0-6c2d8e4b7a10',
'quantity' => 1,
'reason' => 'too_small',
'resolution' => 'refund',
'comment' => 'Runs a bit tight on the shoulders',
],
],
],
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"token": "Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M",
"name": "#1042-R1"
}
}{
"message": "session_expired"
}{
"message": ""
}{
"message": "The route api/oms/v1/public/returns/Acme_Store could not be found."
}{
"message": "exchange_unavailable"
}{
"message": "photo_required"
}{
"message": "window_closed"
}{
"message": "The photos.0 field must be a file of type: jpg, jpeg, png, webp, heic.",
"errors": {
"photos.0": [
"The photos.0 field must be a file of type: jpg, jpeg, png, webp, heic."
]
}
}{
"message": "too_many_requests",
"retry_after": 42
}Return status
Public status page and return label, addressed by the return token received at creation (also
listed in returns[].token of the order view). Treat the token as a secret link: anyone who has it can
see the return and download the label.
/r/{token}Get a return's public status
Everything the shopper needs after creating the return: status in plain words, method and instructions (store and drop-off code with QR code, or carrier and tracking number and whether a label is available), fee, refund or store-credit amount and refund state, the exchange order and any price difference, and the returned lines. Works even if the portal has since been switched off.
Reading the status also refreshes the refund state from the sales channel when a refund is pending.
Rate limit: 60 requests per minute per client (shared portal bucket), 1,800 per minute per store.
Path parameters
tokenstringrequiredReturn token from
POST /returns(data.token) or the order view'sreturns[].token.pattern ^[A-Za-z0-9]{32,64}$Example:Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M
Responses
200Return status.application/json
dataReturnStatusrequiredChild attributes
brandBrandrequiredChild attributes
namestringrequiredExample:Acme Apparellogo_urlstring (uri) | nullrequiredaccent_colorstring | nullrequiredExample:#0F766Esupport_emailstring (email) | nullrequiredsupport_urlstring (uri) | nullrequiredslugstringrequiredExample:acme
languagestringrequiredOne ofitendefresreturnobjectrequiredChild attributes
namestringrequiredExample:#1042-R1order_namestring | nullrequiredExample:#1042statusstringrequiredThe return in the shopper's words:
requested(created, not yet on its way),in_transit,received,checking(received, refund under manual review),completed,refunded,cancelled.One ofrequestedin_transitreceivedcheckingcompletedrefundedcancelledcreated_atstring (date-time) | nullrequiredreceived_atstring (date-time) | nullrequiredrefunded_atstring (date-time) | nullrequiredmethodstring | nullrequiredOne ofcarrier_labelstore_dropoffnullstoreobject | nullrequiredDrop-off store, for
store_dropoff.Child attributes
namestringaddressstring | null
dropoff_codestring | nullrequiredCode to show at the store desk (also encoded in
qr_svg).pattern ^R[A-Z0-9]{4}-[A-Z0-9]{5}$Example:RK7MP-2XQ9Dqr_svgstring | nullrequiredInline SVG QR code of
dropoff_code.carrierstring | nullrequiredReturn carrier name, when a label was generated.
tracking_numberstring | nullrequiredhas_labelbooleanrequiredA label can be downloaded from
/r/{token}/label.currencystring | nullrequiredExample:EURfeenumberrequiredReturn fee withheld.
refund_amountnumberrequiredstore_credit_amountnumberrequiredrefund_statusstring | nullrequiredwaiting(for the items),review(manual check),queued,issued,failed,not_due(nothing to refund, e.g. a straight exchange),none.One ofnonewaitingreviewqueuedissuedfailednot_duenullexchangeobject | nullrequiredThe replacement order, for exchanges.
Child attributes
namestringExample:#1042-EX1modestring | nullstandardships when the return arrives intact;advanceships immediately.One ofstandardadvancenullshippedbooleandifferencenumberReplacement value minus credit from the return (positive = shopper owes).
difference_statusstring | nullOne ofnoneduepaidabsorbedrefundcreditnulllinesarray of objectAttributes of each item
titlestringquantityinteger
linesarray of objectrequiredAttributes of each item
titlestring | nullrequiredsizestring | nullrequiredimage_urlstring (uri) | nullrequiredquantityintegerrequiredreasonstring | nullrequiredReason label in the return's language.
resolutionstringrequiredOne ofrefundexchangestore_creditexchange_titlestring | nullrequired
404Unknown store, malformed or unknown token.application/json
messagestringrequired
429Rate limit exceeded. No
Retry-Afterheader; waitretry_afterseconds.application/jsonmessagestringrequiredOne oftoo_many_requeststoo_many_attemptsretry_afterintegerrequiredSeconds until the limit resets.
curl -X GET https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/r/Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M \
-H 'Accept: application/json'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/r/Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M", {
method: "GET",
headers: {
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/r/Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M', [
'headers' => [
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"brand": {
"name": "Acme Apparel",
"logo_url": "https://cdn.acme.example/logo.svg",
"accent_color": "#0F766E",
"support_email": "help@example.com",
"support_url": "https://shop.acme.example/pages/contact",
"slug": "acme"
},
"language": "it",
"return": {
"name": "#1042-R1",
"order_name": "#1042",
"status": "requested",
"created_at": "2026-10-04T09:15:00+00:00",
"received_at": null,
"refunded_at": null,
"method": "carrier_label",
"store": null,
"dropoff_code": "RK7MP-2XQ9D",
"qr_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 174 174\">…</svg>",
"carrier": "DHL Express",
"tracking_number": "1234567890",
"has_label": true,
"currency": "EUR",
"fee": 4.9,
"refund_amount": 0,
"store_credit_amount": 0,
"refund_status": "waiting",
"exchange": null,
"lines": [
{
"title": "Organic Cotton Tee",
"size": "M",
"image_url": "https://cdn.acme.example/products/tee-blk.jpg",
"quantity": 1,
"reason": "Troppo piccolo",
"resolution": "refund",
"exchange_title": null
}
]
}
}
}{
"data": {
"brand": {
"name": "Acme Apparel",
"logo_url": null,
"accent_color": null,
"support_email": "help@example.com",
"support_url": null,
"slug": "acme"
},
"language": "en",
"return": {
"name": "#1042-R2",
"order_name": "#1042",
"status": "requested",
"created_at": "2026-10-04T09:20:00+00:00",
"received_at": null,
"refunded_at": null,
"method": "store_dropoff",
"store": {
"name": "Acme Apparel Milano",
"address": "Via Roma 1, 20121 Milano"
},
"dropoff_code": "RW3TB-8HJ4N",
"qr_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 174 174\">…</svg>",
"carrier": null,
"tracking_number": null,
"has_label": false,
"currency": "EUR",
"fee": 0,
"refund_amount": 0,
"store_credit_amount": 0,
"refund_status": "waiting",
"exchange": {
"name": "#1042-EX1",
"mode": "standard",
"shipped": false,
"difference": 0,
"difference_status": "none",
"lines": [
{
"title": "Organic Cotton Tee · L",
"quantity": 1
}
]
},
"lines": [
{
"title": "Organic Cotton Tee",
"size": "M",
"image_url": "https://cdn.acme.example/products/tee-blk.jpg",
"quantity": 1,
"reason": "Too small",
"resolution": "exchange",
"exchange_title": "Organic Cotton Tee · L"
}
]
}
}
}{
"message": ""
}{
"message": "too_many_requests",
"retry_after": 42
}/r/{token}/labelDownload the return label
The prepaid return label as a file download: PDF, or ZPL (plain text) for carriers configured
to print thermal labels. Only for carrier_label returns whose label was generated
(has_label: true). Sent with Cache-Control: private, no-store.
Rate limit: 60 requests per minute per client (shared portal bucket), 1,800 per minute per store.
Path parameters
tokenstringrequiredReturn token from
POST /returns(data.token) or the order view'sreturns[].token.pattern ^[A-Za-z0-9]{32,64}$Example:Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M
Responses
200The label.application/pdf
HeadersContent-DispositionAttachment named{return name}-label.pdfor.zpl.Cache-Control
string (binary)
404Unknown store or token, or no label for this return.application/json
messagestringrequired
429Rate limit exceeded. No
Retry-Afterheader; waitretry_afterseconds.application/jsonmessagestringrequiredOne oftoo_many_requeststoo_many_attemptsretry_afterintegerrequiredSeconds until the limit resets.
curl -X GET https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/r/Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M/label \
-H 'Accept: application/pdf'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/r/Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M/label", {
method: "GET",
headers: {
Accept: "application/pdf",
},
});
const data = await res.text();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/returns/acme/r/Xk2Lm9Qa7Rt4Vb1Nc8Wd3Ye6Zf0Gh5Jp2Ks7Lq4M/label', [
'headers' => [
'Accept' => 'application/pdf',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();string{
"message": ""
}{
"message": "too_many_requests",
"retry_after": 42
}