Returns
Create and manage return requests: approve or deny them, buy the return shipping label, mark the package received, and refund the shopper.
A return is a shopper's request to send back items from one of your orders. Shoppers can open returns themselves in your ShipWave returns portal, and you can create them with this API. Both kinds appear in the same list and follow the same lifecycle.
Scopes: GET endpoints need read:returns. POST and PUT need write:returns. A Shipping API key (read:*, write:*) has both. See Authentication.
Return responses can include fields that aren't listed on this page. Rely only on the documented fields; undocumented fields can change or be removed without notice.
Every endpoint sees only the returns and orders on your account. A return ID that belongs to another account returns 404 NOT_FOUND, the same as an ID that doesn't exist.
These endpoints don't send webhook events. Poll Get Return to follow a return.
Return Lifecycle
pending ──approve──▶ approved ──label──▶ label_created ──delivered or receive──▶ received ──refund──▶ refunded
│ └──────────▶ no_action_needed ($0)
└──deny──▶ denied
| Status | Meaning | How a return gets there |
|---|---|---|
pending | Requested and waiting for your decision | Create Return, or the shopper submits it in your returns portal |
approved | Approved, no label yet | Approve Return |
denied | Rejected | Deny Return |
label_created | A return label was purchased | Create Return Label |
shipped | The shopper shipped the package | Set automatically for returns synced from a connected marketplace |
received | The package arrived | Mark Return Received, or automatically when the carrier reports a ShipWave return label delivered |
refunded | The refund was issued | Refund Return |
no_action_needed | Closed with a $0 refund | Refund Return when nothing is owed after fees |
expired | Closed with no refund because the return label went unused | Can be set automatically when the carrier still hasn't picked up the package 14 days after the label was purchased. ShipWave voids the unused label when the carrier allows it. |
cancelled | Closed with no refund | ShipWave dashboard |
exchanged | Resolved with an exchange instead of a refund | ShipWave dashboard |
invoice_created, invoice_sent | The shopper was billed for an item that wasn't sent back | ShipWave dashboard, or automatically |
A delivered return label moves the return to received from label_created or shipped. If the label of an expired or cancelled return is delivered anyway, the return reopens as received. A delivery never refunds the shopper automatically. ShipWave emails you, and you refund with Refund Return.
Return Shipping and Labels
When you call Create Return Label, ShipWave rates the parcel from the shopper's address to your return warehouse, buys the cheapest available rate right away, and returns the label URLs. It doesn't email the label to the shopper. Send labelUrl (or the PDF or PNG version) to the shopper yourself. Calling it again for the same return returns the same label; it never buys a second one unless you ask it to replace the first.
The shopper doesn't pay anything when the label is created. If your return policy doesn't offer free returns, ShipWave records a return shipping fee on the return (shippingFeeCharged) when the label is purchased, and deducts it from the shopper's refund. International returns always carry a return shipping fee, even when your policy offers free returns. When the return ships free, the fee is 0.
Shopper Notifications
Creating, approving, denying, labeling, and receiving a return don't email the shopper. Refund Return emails the shopper a refund receipt when it issues a refund greater than $0 (except for Walmart returns, which Walmart refunds).
The Return Object
Get Return returns these fields. List Returns returns a subset.
| Field | Type | Description |
|---|---|---|
id | string | Return ID |
orderNumber | string | Order number the return was opened against |
orderDate | string | null | ISO 8601 order date (Get Return only) |
customerEmail | string | Shopper's email address |
customerName | string | null | Shopper's name |
status | string | See Return Lifecycle |
reason | string | Return reason. The returns portal uses wrong_item, defective, not_as_described, changed_mind, and other. |
reasonDetail | string | null | The shopper's explanation |
items | array | Items being returned, as they were submitted, each with a resolved price (below) |
refundAmount | number | null | Amount refunded. null until the return is refunded or closed. |
shippingFeeCharged | number | Return shipping fee deducted (or to be deducted) from the refund |
restockingFeeCharged | number | Restocking fee deducted from the refund. 0 until the refund is processed. |
returnTrackingNumber | string | null | Tracking number of the return label |
returnCarrier | string | null | Carrier of the return label |
returnLabelUrl | string | null | URL of the return label (Get Return only) |
adminNotes | string | null | Internal notes, kept as a log of timestamped lines ([YYYY-MM-DD HH:MM:SS] ..., UTC). Notes you send are added as Note: lines, and ShipWave adds its own lines when it refunds or closes a return. Nothing is ever replaced or removed. (Get Return only) |
denialReason | string | null | Reason given when the return was denied (Get Return only) |
requestedAt | string | ISO 8601 time the return was requested |
approvedAt, deniedAt, shippedAt, receivedAt, refundedAt | string | null | ISO 8601 time of each lifecycle step |
labelCreatedAt | string | null | ISO 8601 time the return label was purchased (Get Return only) |
order | object | null | The linked order (Get Return only). null if the return isn't linked to an order. |
returnShipment | object | null | The return label's shipment (Get Return only). null before a label exists. |
Item object:
| Field | Type | Description |
|---|---|---|
lineItemId | string | Order line item ID, when it was provided |
sku | string | SKU, when it was provided |
name | string | Item name, when it was provided |
quantity | integer | Quantity being returned |
price | number | Unit price. A price stored with the item wins. Otherwise ShipWave uses the price the shopper paid for the matching order line (matched by lineItemId, then sku, then name), or 0 when nothing matches. |
Items created with this API store only these fields and an optional reason. Items from your returns portal or a connected marketplace can carry other fields.
List Returns
GET /api/v1/returns
Returns a page of your returns, most recently requested first, plus a count of all your returns by status.
Response shape: unlike other list endpoints, this one doesn't use the standard
data/metaenvelope. The page of returns is inreturns, the paging info is inpagination, and the per-status totals are incounts. Thepageandlimitparameters work as described in Pagination.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (minimum 1) |
limit | integer | 50 | Items per page (1–100; larger values are reduced to 100) |
status | string | — | Only returns with this exact status. all or no value returns every status. An unknown status returns an empty list. |
search | string | — | Case-insensitive partial match on order number, shopper email, or shopper name |
Example
curl "https://shipwave.app/api/v1/returns?status=label_created&limit=25" \
-H "Authorization: Bearer sw_live_abc123..."
const res = await fetch(
"https://shipwave.app/api/v1/returns?status=label_created&limit=25",
{ headers: { Authorization: "Bearer sw_live_abc123..." } }
);
const { returns, pagination, counts } = await res.json();
Response
{
"returns": [
{
"id": "clx7ret1",
"orderNumber": "#1042",
"customerEmail": "jane@example.com",
"customerName": "Jane Doe",
"status": "label_created",
"reason": "defective",
"reasonDetail": "Left side stopped working after two days",
"items": [
{
"lineItemId": "clx1item1",
"sku": "WIDGET-001",
"name": "Blue Widget",
"quantity": 1,
"price": 49.99
}
],
"refundAmount": null,
"shippingFeeCharged": 0,
"restockingFeeCharged": 0,
"returnTrackingNumber": "9400111899223100012345",
"returnCarrier": "USPS",
"requestedAt": "2026-10-05T16:12:44.000Z",
"approvedAt": "2026-10-05T18:02:10.000Z",
"deniedAt": null,
"shippedAt": null,
"receivedAt": null,
"refundedAt": null
}
],
"pagination": {
"page": 1,
"limit": 25,
"total": 1,
"totalPages": 1
},
"counts": {
"all": 14,
"pending": 2,
"approved": 1,
"label_created": 3,
"shipped": 0,
"received": 1,
"no_action_needed": 0,
"expired": 1,
"invoice_created": 0,
"invoice_sent": 0,
"refunded": 5,
"denied": 1,
"cancelled": 0,
"exchanged": 0
}
}
pagination.total counts the returns that match your filters. counts always covers every return on your account and ignores status and search. It has a key for every status in Return Lifecycle, and the per-status counts add up to all.
For 401, 403, and 429 responses, see Errors.
Create Return
POST /api/v1/returns
Creates a return in pending status. ShipWave looks up the order by orderNumber, checks the request against your return window, and saves a copy of your return policy terms on the return. The restocking fee rate ShipWave later deducts comes from that copy, so changing your return policy doesn't re-price returns that are already open.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
orderNumber | string | yes | The order's number. The leading # is optional and matching is case-insensitive, so 1042 and #1042 both find order #1042. The return stores the order's own number. |
customerEmail | string | yes | Shopper's email address (up to 320 characters) |
customerName | string | yes | Shopper's name (up to 200 characters) |
reason | string | yes | Return reason, for example defective (up to 500 characters). The returns portal uses wrong_item, defective, not_as_described, changed_mind, and other. |
reasonDetail | string | no | The shopper's explanation (up to 5,000 characters) |
items | array | no | Items being returned, up to 100 (defaults to an empty list). See Item fields below. |
itemCondition | string | no | unopened or opened. Used only when your return policy charges different restocking fees for unopened and opened items; otherwise it's stored as null. |
returnWindowOverride | object | no | Needed only when the order is past its return window and has no active override from Reopen Return Window. See below. |
adminNotes | string | no | A first internal note (up to 4,000 characters), saved as a timestamped Note: line |
Item fields:
| Field | Type | Required | Description |
|---|---|---|---|
lineItemId | string | one of these three | The order line's id. It must be a line on this order. |
sku | string | one of these three | SKU |
name | string | one of these three | Item name |
quantity | integer | no | Whole number from 1 to 10,000 (default 1) |
price | number | no | Unit price, 0 or more. Leave it out to use the price the shopper paid for the matching order line. |
reason | string | no | Reason for this item (up to 500 characters) |
Other item fields are dropped. Text fields can be up to 500 characters.
returnWindowOverride object:
| Field | Type | Required | Description |
|---|---|---|---|
confirmed | boolean | yes | Must be true |
reason | string | yes | Why you're accepting a late return (up to 1,000 characters are kept) |
reference | string | no | Your own reference, such as a ticket number (up to 200 characters are kept) |
Things to know:
- The order must exist on your account. If no order matches
orderNumber, the request fails with404 NOT_FOUNDand no return is created. - Later steps depend on the items. Create Return Label needs each item's
skuto match a product with a weight on file, and Refund Return needs a price above $0 for each item (sent with the item, or resolved from the order line). - The return window is your return policy's window (30 days if you haven't set one). It starts on the order's latest carrier-confirmed delivery date, or the order date when there's no delivery, and counts calendar days: the last day ends at 11:59:59 PM in the
timeZoneshown in the response (currentlyAmerica/New_Yorkfor every store). A return requested after that needs either an active override from Reopen Return Window or a confirmedreturnWindowOverride; otherwise the request fails with409 CONFLICT. An override is ignored for an order still inside its window.
Example
curl -X POST https://shipwave.app/api/v1/returns \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"orderNumber": "#1042",
"customerEmail": "jane@example.com",
"customerName": "Jane Doe",
"reason": "defective",
"reasonDetail": "Left side stopped working after two days",
"items": [
{ "lineItemId": "clx1item1", "sku": "WIDGET-001", "name": "Blue Widget", "quantity": 1 }
],
"itemCondition": "unopened"
}'
const res = await fetch("https://shipwave.app/api/v1/returns", {
method: "POST",
headers: {
Authorization: "Bearer sw_live_abc123...",
"Content-Type": "application/json",
},
body: JSON.stringify({
orderNumber: "#1042",
customerEmail: "jane@example.com",
customerName: "Jane Doe",
reason: "defective",
reasonDetail: "Left side stopped working after two days",
items: [
{ lineItemId: "clx1item1", sku: "WIDGET-001", name: "Blue Widget", quantity: 1 },
],
itemCondition: "unopened",
}),
});
const { data } = await res.json(); // 201 Created
Response (201 Created)
{
"data": {
"id": "clx7ret1",
"orderNumber": "#1042",
"customerEmail": "jane@example.com",
"customerName": "Jane Doe",
"status": "pending",
"reason": "defective",
"reasonDetail": "Left side stopped working after two days",
"items": [
{ "lineItemId": "clx1item1", "sku": "WIDGET-001", "name": "Blue Widget", "quantity": 1 }
],
"itemCondition": "unopened",
"restockingFeePercent": 10,
"requestedAt": "2026-10-05T16:12:44.000Z",
"returnWindow": {
"deliveredAt": "2026-09-28T19:03:00.000Z",
"returnDeadline": "2026-10-29T03:59:59.999Z",
"returnDeadlineDate": "2026-10-28",
"timeZone": "America/New_York",
"returnWindowDays": 30,
"daysOutsideWindow": 0,
"exceptionApplied": false
},
"order": {
"id": "clx1ord1",
"orderNumber": "#1042",
"totalValue": 54.43
}
}
}
| Field | Description |
|---|---|
items | The items as stored: the fields listed above, trimmed, with quantity filled in. Prices are resolved in Get Return. |
restockingFeePercent | Restocking fee rate quoted for this return. With a condition-tiered policy, a return created without itemCondition is quoted at the opened rate. |
returnWindow.deliveredAt | Delivery date the window started from, or null when it started from the order date |
returnWindow.returnDeadline | Last moment a return is on time (ISO 8601) |
returnWindow.returnDeadlineDate | Last on-time calendar day, YYYY-MM-DD in timeZone |
returnWindow.daysOutsideWindow | Calendar days the request is past the deadline (0 when on time) |
returnWindow.exceptionApplied | true when the order was past its window and was accepted because of your returnWindowOverride or an active window override |
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is missing, isn't valid JSON, or isn't a JSON object |
| 400 | VALIDATION_ERROR | A required field is missing or empty (for example reason is required), a field has the wrong type or is too long, or customerEmail isn't an email address |
| 400 | VALIDATION_ERROR | An item is invalid. The message names it, for example items[0].quantity must be a whole number from 1 to 10000, or a lineItemId that isn't on the order. |
| 400 | VALIDATION_ERROR | itemCondition must be "unopened" or "opened", or returnWindowOverride isn't an object |
| 404 | NOT_FOUND | No order with this number on your account (Order not found) |
| 409 | CONFLICT | orderNumber matches more than one of your orders and none exactly. Send it exactly as the Orders API returns it. details.orderNumbers lists the matches. |
| 409 | CONFLICT | The order is past its return window, has no active window override, and the body has no confirmed returnWindowOverride with a reason. details describes the window (example below). |
Return window conflict:
{
"error": {
"code": "CONFLICT",
"message": "This order is past the 30-day return window. To approve an exception, confirm the policy override and provide a reason.",
"details": {
"deliveredAt": "2026-08-20T17:45:00.000Z",
"returnDeadline": "2026-09-20T03:59:59.999Z",
"returnDeadlineDate": "2026-09-19",
"timeZone": "America/New_York",
"returnWindowDays": 30,
"daysOutsideWindow": 16,
"overrideRequired": true
}
}
}
For 401, 403, and 429 responses, see Errors.
Get Return
GET /api/v1/returns/:id
Returns one return with its linked order and return shipment.
Example
curl https://shipwave.app/api/v1/returns/clx7ret1 \
-H "Authorization: Bearer sw_live_abc123..."
Response
{
"data": {
"id": "clx7ret1",
"orderNumber": "#1042",
"orderDate": "2026-09-24T14:31:07.000Z",
"customerEmail": "jane@example.com",
"customerName": "Jane Doe",
"status": "label_created",
"reason": "defective",
"reasonDetail": "Left side stopped working after two days",
"items": [
{
"lineItemId": "clx1item1",
"sku": "WIDGET-001",
"name": "Blue Widget",
"quantity": 1,
"price": 49.99
}
],
"refundAmount": null,
"shippingFeeCharged": 0,
"restockingFeeCharged": 0,
"returnTrackingNumber": "9400111899223100012345",
"returnCarrier": "USPS",
"returnLabelUrl": "https://files.example.com/labels/clx7ret1.png",
"adminNotes": "[2026-10-05 18:02:10] Note: Approved: defect confirmed from photos",
"denialReason": null,
"requestedAt": "2026-10-05T16:12:44.000Z",
"approvedAt": "2026-10-05T18:02:10.000Z",
"deniedAt": null,
"labelCreatedAt": "2026-10-05T18:04:31.000Z",
"shippedAt": null,
"receivedAt": null,
"refundedAt": null,
"order": {
"id": "clx1ord1",
"orderNumber": "#1042",
"totalValue": 54.43,
"lineItems": [
{
"id": "clx1item1",
"sku": "WIDGET-001",
"name": "Blue Widget",
"quantity": 1,
"weight": 16,
"price": 49.99,
"imageUrl": "https://cdn.shopify.com/s/files/1/widget-blue.jpg"
}
]
},
"returnShipment": {
"id": "clx2ship9",
"trackingCode": "9400111899223100012345",
"trackingStatus": "pre_transit",
"carrier": "USPS",
"service": "GroundAdvantage"
}
}
}
order.lineItems lists every line on the order, not only the returned items, with the same fields as order lines in the Orders API. returnShipment.id is a shipment ID you can use with the Shipments API.
Errors
| Status | Code | Cause |
|---|---|---|
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found). A return that belongs to another account also returns 404. |
For 401, 403, and 429 responses, see Errors.
Add Return Note
PUT /api/v1/returns/:id
Adds a note to the return's internal notes, in any status. Notes are a log: your note is appended as a new timestamped Note: line, and existing notes, including the entries ShipWave writes when it refunds a return, are never replaced or removed. A note identical to the last line isn't added twice.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
adminNotes | string | no | The note to add (up to 4,000 characters). An empty string or a missing field adds nothing and returns the current notes. |
Other fields are ignored, except refundAmount, which is rejected. Use Refund Return to refund. Notes can't be cleared, so null is rejected.
Example
curl -X PUT https://shipwave.app/api/v1/returns/clx7ret1 \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{ "adminNotes": "Shopper sent photos of the defect" }'
Response
{
"data": {
"id": "clx7ret1",
"adminNotes": "[2026-10-05 16:20:03] Note: Shopper sent photos of the defect",
"refundAmount": null
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is missing, isn't valid JSON, or isn't a JSON object |
| 400 | VALIDATION_ERROR | The body includes refundAmount (refundAmount is read-only. Process refunds through POST /api/v1/returns/:id/refund.) |
| 400 | VALIDATION_ERROR | adminNotes isn't a string, or is longer than 4,000 characters |
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found) |
| 409 | CONFLICT | The notes kept changing while your note was being added. Fetch the return and try again. |
For 401, 403, and 429 responses, see Errors.
Approve Return
POST /api/v1/returns/:id/approve
Approves a pending return and moves it to approved. Approving doesn't buy a label or set the refund amount.
If the return was requested after its return window closed and no exception was recorded when it was created, you need to confirm one now with returnWindowOverride, unless the order has an active override from Reopen Return Window. The window is checked in the timezone recorded when the return was created.
Only a return that is still pending is approved. If another request approves or denies it first, you get 409 CONFLICT.
Request Body
All fields are optional, and the body can be empty.
| Field | Type | Description |
|---|---|---|
adminNotes | string | A note to add to the return's internal notes (see Add Return Note) |
returnWindowOverride | object | { "confirmed": true, "reason": "...", "reference": "..." }, as in Create Return. Needed only for a late return without a recorded exception or an active window override. |
A body that includes refundAmount is rejected.
Example
curl -X POST https://shipwave.app/api/v1/returns/clx7ret1/approve \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{ "adminNotes": "Approved: defect confirmed from photos" }'
Response
{
"data": {
"id": "clx7ret1",
"status": "approved",
"approvedAt": "2026-10-05T18:02:10.000Z",
"refundAmount": null
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body isn't valid JSON or isn't a JSON object |
| 400 | VALIDATION_ERROR | The body includes refundAmount (refundAmount is read-only. Approval does not set the eventual refund amount.) |
| 400 | VALIDATION_ERROR | adminNotes isn't a string or is too long, or returnWindowOverride isn't an object |
| 400 | VALIDATION_ERROR | The return isn't pending (Cannot approve return with status: approved) |
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found) |
| 409 | CONFLICT | The return was requested after its window closed, no exception or active window override was recorded, and the body has no confirmed returnWindowOverride with a reason. details has the same fields as the Create Return conflict. |
| 409 | CONFLICT | Another request changed the return while it was being approved (for example, it was denied). details.status is its current status. |
For 401, 403, and 429 responses, see Errors.
Deny Return
POST /api/v1/returns/:id/deny
Denies a pending return and moves it to denied. The shopper isn't notified. If another request approves or denies the return first, you get 409 CONFLICT.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
denialReason | string | yes | Why the return was denied (up to 2,000 characters) |
adminNotes | string | no | A note to add to the return's internal notes (see Add Return Note) |
Example
curl -X POST https://shipwave.app/api/v1/returns/clx7ret1/deny \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{ "denialReason": "Item was used and is outside the return policy" }'
Response
{
"data": {
"id": "clx7ret1",
"status": "denied",
"deniedAt": "2026-10-05T18:02:10.000Z",
"denialReason": "Item was used and is outside the return policy"
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is missing, isn't valid JSON, or isn't a JSON object |
| 400 | VALIDATION_ERROR | denialReason is required (missing or empty), or denialReason isn't a string or is longer than 2,000 characters. This is checked before the return is looked up. |
| 400 | VALIDATION_ERROR | adminNotes isn't a string or is too long |
| 400 | VALIDATION_ERROR | The return isn't pending (Cannot deny return with status: approved) |
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found) |
| 409 | CONFLICT | Another request changed the return while it was being denied (for example, it was approved). details.status is its current status. |
For 401, 403, and 429 responses, see Errors.
Create Return Label
POST /api/v1/returns/:id/label
Buys a prepaid 4×6 return label from the shopper to your return warehouse and moves the return to label_created. The return must be approved (or already label_created, below) and linked to an order. See Return Shipping and Labels for how the label is paid for and delivered.
How ShipWave builds the shipment:
- From:
customerAddressif you send it. Otherwise the ship-to address of the order's first shipment, with the return'scustomerName. If yourcustomerAddresshas nophone, ShipWave uses the phone on the order's shipping address. When the shopper has no phone at all, your return warehouse's phone goes on the label instead, because carriers such as FedEx require one. - To: your default warehouse, if it has a complete address. Otherwise the first warehouse, alphabetically by name, that has one. The label always goes to one of your own warehouses; you can't choose another destination.
- Parcel: the weight and dimensions you send. Otherwise the box the shopper described in the returns portal, then the catalog weight and dimensions of the returned items. Every returned item that requires shipping must have a SKU that matches a product with a weight on file, even when you send
parcelWeight. - Rate: the cheapest available rate. FedEx is skipped when either phone number isn't a valid US or Canada number. International returns include customs paperwork, which needs an HS code, country of origin, weight, and value for every item (see Products).
Calling this endpoint again is safe:
- The return already has a label (
label_created): you get that label back withidempotent: true, and nothing is bought.labelPdfUrlandlabelPngUrlarenullandallRatesis empty in that response. - To replace the label, send
replaceExistingLabel: true. ShipWave voids the current label with the carrier first and then buys a new one. If the carrier won't void it (for example, the package is already moving), the request fails with409 CONFLICTand nothing is bought. - Two requests at once: only one buys a label. The other gets
409 CONFLICT.
Request Body
All fields are optional, and the body can be empty.
| Field | Type | Description |
|---|---|---|
customerAddress | object | Address the shopper ships from (below) |
parcelWeight | number | Parcel weight in ounces |
parcelLength | number | Parcel length in inches |
parcelWidth | number | Parcel width in inches |
parcelHeight | number | Parcel height in inches |
replaceExistingLabel | boolean | true to void the return's current label and buy a new one. Ignored for an approved return. |
Parcel values must be positive numbers; 0 is ignored.
customerAddress object:
| Field | Type | Required |
|---|---|---|
name | string | no |
street1 | string | yes |
street2 | string | no |
city | string | yes |
state | string | yes |
zip | string | yes |
country | string | no (defaults to US) |
phone | string | no |
When you send customerAddress, it must include street1, city, state, and zip, and every field must be a string. The carrier validates the address itself when it rates the shipment.
Example
curl -X POST https://shipwave.app/api/v1/returns/clx7ret1/label \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"customerAddress": {
"name": "Jane Doe",
"street1": "742 Evergreen Ter",
"city": "Springfield",
"state": "OR",
"zip": "97477",
"country": "US",
"phone": "541-555-0123"
}
}'
Response
This example is for a store whose return policy offers free returns, so shippingFee is 0:
{
"data": {
"label": {
"trackingNumber": "9400111899223100012345",
"carrier": "USPS",
"service": "GroundAdvantage",
"labelUrl": "https://files.example.com/labels/clx7ret1.png",
"labelPdfUrl": null,
"labelPngUrl": "https://files.example.com/labels/clx7ret1.png",
"shippingFee": 0,
"isInternational": false,
"returnedGoodsTreatment": null,
"customerPaysReturnShipping": false,
"originalShippingRefundable": false,
"customsFormUrl": null,
"customsFormType": null,
"customsFormSubmittedElectronically": null
},
"allRates": [
{
"carrier": "USPS",
"service": "GroundAdvantage",
"deliveryDays": 3
},
{
"carrier": "UPS",
"service": "Ground",
"deliveryDays": 4
}
],
"idempotent": false,
"replacedLabel": null
}
}
| Field | Description |
|---|---|
label.labelUrl | Label to send to the shopper |
label.labelPdfUrl | The label as a PDF, when the carrier provided one. Otherwise null. |
label.labelPngUrl | The label as a PNG (the same file as labelUrl when it is a PNG). Otherwise null. |
label.shippingFee | Return shipping fee saved on the return as shippingFeeCharged and deducted from the shopper's refund. 0 when the return ships free. If you already changed the return's shipping fee in the ShipWave dashboard, that amount is kept for a domestic return. |
label.customerPaysReturnShipping | true when the shopper bears return shipping: your policy doesn't offer free returns, or the return is international |
label.originalShippingRefundable | Always false. Refunds cover the returned items and their tax, not the original shipping charge. |
label.isInternational | true when the shopper's country differs from your warehouse's |
label.returnedGoodsTreatment | For an international return of goods originally shipped from the US, the returned-goods customs declaration ShipWave filed. Otherwise null. |
label.customsFormUrl, customsFormType, customsFormSubmittedElectronically | The commercial invoice for an international return, when the carrier produced one. null for domestic returns. |
allRates | The carrier services that quoted this return, cheapest first. ShipWave bought the first one it could use (FedEx is skipped when a phone number isn't a valid US or Canada number). |
idempotent | true when the return already had a label and this response returns it without buying another |
replacedLabel | With replaceExistingLabel: true, the label that was voided: trackingNumber and the carrier's voidStatus. Otherwise null. |
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body isn't valid JSON or isn't a JSON object, customerAddress is incomplete or not an object, a parcel value isn't a positive number, or replaceExistingLabel isn't a boolean |
| 400 | VALIDATION_ERROR | The return isn't approved or label_created (Cannot create label for return with status: pending) |
| 400 | VALIDATION_ERROR | No warehouse configured for returns: none of your warehouses has a complete address |
| 400 | VALIDATION_ERROR | Customer address not found. Please provide address manually.: no customerAddress and no shipping address on the order |
| 400 | VALIDATION_ERROR | Return request is not linked to an order |
| 400 | VALIDATION_ERROR | A returned item has no weight on file, or a product that needs catalog dimensions has none |
| 400 | VALIDATION_ERROR | Cannot generate label. Missing weight data. |
| 400 | VALIDATION_ERROR | No shipping rates available for this return |
| 400 | VALIDATION_ERROR | FedEx is the only usable rate but a phone number is missing or isn't a valid US or Canada number (the message says which side) |
| 400 | VALIDATION_ERROR | The shopper has no phone number and your return warehouse address has none to use instead. Add a phone to either and try again. |
| 400 | VALIDATION_ERROR | An international return is missing customs data. The message lists what's missing, for example an item's HS code or country of origin. |
| 400 | VALIDATION_ERROR | The carrier rejected the shipment or the purchase, for example an address it can't verify. The message includes the carrier's reason. |
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found) |
| 409 | CONFLICT | The return shares one label with another return. Manage that label in the ShipWave dashboard. |
| 409 | CONFLICT | Another label request for this return is in progress, or an earlier purchase didn't finish. Fetch the return before trying again. |
| 409 | CONFLICT | replaceExistingLabel was sent but the current label couldn't be voided. Nothing was bought. |
| 502 | INTERNAL_ERROR | The carrier service couldn't rate or buy the label right now, or a foreign-currency rate couldn't be converted to USD. Nothing was bought; try again shortly. |
For 401, 403, and 429 responses, see Errors.
Mark Return Received
POST /api/v1/returns/:id/receive
Marks a label_created or shipped return as received. Returns that use a ShipWave label are usually marked received automatically when the carrier reports delivery, so you mostly need this for packages that arrive before tracking updates. Receiving doesn't refund the shopper.
Request Body
The body can be empty.
| Field | Type | Description |
|---|---|---|
adminNotes | string | A note to add to the return's internal notes (see Add Return Note) |
Example
curl -X POST https://shipwave.app/api/v1/returns/clx7ret1/receive \
-H "Authorization: Bearer sw_live_abc123..."
Response
{
"data": {
"id": "clx7ret1",
"status": "received",
"receivedAt": "2026-10-09T15:40:22.000Z"
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body isn't valid JSON or isn't a JSON object, or adminNotes isn't a string or is too long |
| 400 | VALIDATION_ERROR | The return isn't label_created or shipped (Cannot mark as received with status: approved) |
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found) |
| 409 | CONFLICT | The return changed while it was being marked received, for example because the carrier's delivery scan received it first. details.status is its current status. |
For 401, 403, and 429 responses, see Errors.
Refund Return
POST /api/v1/returns/:id/refund
Refunds a received return. ShipWave calculates the amount; you can't send one.
refund = returned items + sales tax on those items − restocking fee − return shipping fee
- Returned items are valued at the price the shopper paid, after discounts.
- Sales tax on the returned items is confirmed against the Shopify order.
- Restocking fee is a percentage of the items plus tax, at the rate saved on the return when it was created, so a later policy change doesn't re-price it. If your policy charges different rates for unopened and opened items, the item condition picks the saved rate, and a return with no condition recorded uses the opened rate. Only an older return with no saved rate uses your current policy rate. A restocking fee you already changed in the ShipWave dashboard is kept. Before ShipWave deducts a restocking fee, the return needs an inspection recorded in the ShipWave dashboard, or you must confirm skipping it with
inspectionSkip. - Return shipping fee is the fee recorded on the return (
shippingFeeCharged), normally set when the return label was purchased.
A refund above $0 is issued on the linked Shopify order to the original payment method, the return moves to refunded, and ShipWave emails the shopper a receipt under your store name (your return policy's merchant name). When nothing is owed after fees, no refund is issued, no email is sent, and the return moves to no_action_needed.
Returns from a connected Walmart channel are refunded through Walmart for the returned items' amount, with no fees deducted. feeOverride and inspectionSkip are ignored for them, and the response includes a walmartRefund object.
Each return can be refunded once, and retrying is safe:
- After a refund succeeded, a retry returns the stored result with
idempotent: true, and the shopper isn't refunded again. - After Shopify rejected a refund (
422,Shopify refund failed: ...), fix the cause and call again. Before retrying, ShipWave checks the order's refunds in Shopify. If the failed attempt made no refund, it refunds now. If Shopify shows that the attempt did refund the shopper, ShipWave records that refund and closes the return without refunding twice (idempotent: true). If Shopify shows a refund ShipWave can't match to the attempt, nothing is refunded and you get409 CONFLICT; contact ShipWave support to reconcile the return. - While an attempt is still in progress, other calls return
409 CONFLICT. Wait and fetch the return before trying again.
Request Body
All fields are optional, and the body can be empty.
| Field | Type | Description |
|---|---|---|
adminNotes | string | A note to add to the return's internal notes (up to 4,000 characters), before the refund entry ShipWave adds |
feeOverride | object | Changes the restocking fee, the return shipping fee, or both (below) |
inspectionSkip | object | Confirms refunding without a recorded inspection (below). Needed only when a restocking fee applies. |
Bodies that include finalRefundAmount, refundAmount, or amount are rejected.
feeOverride object:
| Field | Type | Required | Description |
|---|---|---|---|
confirmed | boolean | yes | Must be true |
reason | string | yes | Why you're changing the fees (up to 1,000 characters are kept) |
restockingFee | number | one of these two | Restocking fee to deduct, in dollars (0 or more) |
shippingFee | number | one of these two | Return shipping fee to deduct, in dollars (0 or more). For an international return it can't be lower than the cost of the purchased label. |
Fees can't add up to more than the returned items plus tax. Fee overrides aren't allowed on a return-first exchange from the returns portal, where the refund is only the price difference owed to the shopper.
inspectionSkip object:
| Field | Type | Required | Description |
|---|---|---|---|
confirmed | boolean | yes | Must be true |
reason | string | yes | Why no inspection was recorded (up to 1,000 characters are kept) |
Example
curl -X POST https://shipwave.app/api/v1/returns/clx7ret1/refund \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"inspectionSkip": {
"confirmed": true,
"reason": "Checked on arrival; box unopened"
}
}'
Response
{
"data": {
"id": "clx7ret1",
"status": "refunded",
"refundedAt": "2026-10-09T16:05:12.000Z",
"itemsSubtotal": 49.99,
"itemsTax": 4.44,
"defaultShippingFee": 0,
"defaultRestockingFee": 5.44,
"shippingFeeDeducted": 0,
"restockingFeeDeducted": 5.44,
"finalRefundAmount": 48.99,
"feeOverrideApplied": false,
"inspectionSkipped": true,
"shopifyRefundId": "1029384756",
"refundReceiptEmailSent": true,
"dutyRefund": null,
"idempotent": false
}
}
| Field | Description |
|---|---|
status | refunded, or no_action_needed when the refund is $0 |
refundedAt | ISO 8601 refund time. null for no_action_needed. |
itemsSubtotal | Returned items at the price paid |
itemsTax | Sales tax on the returned items |
defaultShippingFee, defaultRestockingFee | Fees ShipWave would deduct without a feeOverride |
shippingFeeDeducted, restockingFeeDeducted | Fees actually deducted |
finalRefundAmount | Amount refunded to the shopper |
feeOverrideApplied | true when your feeOverride was used |
inspectionSkipped | true when the refund went ahead without a recorded inspection |
shopifyRefundId | ID of the refund on the Shopify order. Omitted when no Shopify refund was made. |
refundReceiptEmailSent | Whether the receipt email reached the mail server |
dutyRefund | For orders where duties and import taxes were collected at checkout, the result of refunding them: an object whose mode is refunded, skipped, or failed. null otherwise. |
idempotent | true when this response replays an earlier successful refund |
A replayed response (idempotent: true) has the same fields as the first one. A Walmart refund response doesn't include itemsTax or inspectionSkipped.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body isn't valid JSON or isn't a JSON object, or includes finalRefundAmount, refundAmount, or amount |
| 400 | VALIDATION_ERROR | adminNotes isn't a string or is longer than 4,000 characters |
| 400 | VALIDATION_ERROR | feeOverride or inspectionSkip isn't an object, a fee isn't a number of 0 or more, or feeOverride changes neither fee |
| 400 | VALIDATION_ERROR | The fees add up to more than the returned items plus tax, or an international return's shipping fee is below the label cost |
| 404 | NOT_FOUND | No return with this ID on your account (Return request not found) |
| 409 | CONFLICT | The return isn't received (Cannot refund return with status: label_created. Must be "received" first.) |
| 409 | CONFLICT | feeOverride or inspectionSkip is missing confirmed: true or a reason |
| 409 | CONFLICT | A restocking fee applies, no inspection is recorded, and there's no inspectionSkip |
| 409 | CONFLICT | The return already has an exchange or replacement order and can't also be refunded |
| 409 | CONFLICT | The calculated refund is more than the order's remaining refundable balance (after earlier refunds and lost chargebacks) |
| 409 | CONFLICT | An earlier refund attempt for this return is still in progress, or another request is retrying it |
| 409 | CONFLICT | An earlier attempt failed and Shopify shows a refund on the order that ShipWave can't match to it. Nothing was refunded. Contact ShipWave support. |
| 409 | CONFLICT | Shopify issued the refund but ShipWave couldn't finish recording it. Don't retry; contact ShipWave support. |
| 409 | CONFLICT | A return-first exchange has no price difference owed to the shopper, or a feeOverride was sent for one |
| 409 | CONFLICT | A Walmart return isn't linked to its channel, or the channel is disconnected |
| 422 | VALIDATION_ERROR | The return has no items, or an item has a missing or $0 price (other than a line that was discounted 100%) or a quantity that isn't a positive whole number |
| 422 | VALIDATION_ERROR | Sales tax couldn't be confirmed with Shopify, so the refund was held |
| 422 | VALIDATION_ERROR | A refund above $0 needs a linked Shopify order, and the return has none (or the store connection is incomplete) |
| 422 | VALIDATION_ERROR | A returned item couldn't be matched to a Shopify order line, so nothing was refunded |
| 422 | VALIDATION_ERROR | Shopify rejected the refund (Shopify refund failed: ...) |
| 502 | INTERNAL_ERROR | Walmart rejected the refund, or ShipWave couldn't reach Shopify to check an earlier failed attempt. Nothing was refunded. |
| 500 | INTERNAL_ERROR | Unexpected error (Failed to process refund) |
For 401, 403, and 429 responses, see Errors.
Get Return Window
GET /api/v1/returns/window-overrides?orderNumber=:orderNumber
Shows one order's return window, any active window override, any open return, and your returns portal link. Use it before Reopen Return Window.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
orderNumber | string | yes | Order number. The leading # is optional and matching is case-insensitive. |
Example
curl "https://shipwave.app/api/v1/returns/window-overrides?orderNumber=1007" \
-H "Authorization: Bearer sw_live_abc123..."
Response
{
"data": {
"order": {
"id": "clx1ord2",
"orderNumber": "#1007"
},
"returnWindow": {
"returnDeadline": "2026-09-20T03:59:59.999Z",
"returnDeadlineDate": "2026-09-19",
"isOutsideWindow": true,
"daysOutsideWindow": 16
},
"activeOverride": null,
"openReturn": null,
"portalUrl": "https://shipwave.app/returns/acme"
}
}
| Field | Description |
|---|---|
returnWindow | The order's window as of now, calculated as in Create Return. null if the order has no dates to start from. |
activeOverride | The unexpired override with the latest expiry: id, grantedAt, expiresAt, reason, reference. null when there's none. |
openReturn | { "id": "...", "status": "..." } for a return on this order that isn't denied, refunded, no_action_needed, or cancelled. null when there's none. |
portalUrl | Your returns portal, or null if you haven't set one up |
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | orderNumber is required |
| 404 | NOT_FOUND | No order with this number on your account (Order not found) |
For 401, 403, and 429 responses, see Errors.
Reopen Return Window
POST /api/v1/returns/window-overrides
Reopens your returns portal for one order whose return window has closed, so the shopper can submit the return themselves. The override lasts through the end of the 14th calendar day after you grant it. While it's active, Create Return and Approve Return accept the order's late return through the API too, without a returnWindowOverride.
- Already reopened: if the order has an active override, you get that override back (
200 OK,alreadyActive: true) and no new one is created. - Still inside its window: the shopper can already use the portal, so no override is created and you get
409 CONFLICTwith the window's state indetails.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
orderNumber | string | yes | Order number. The leading # is optional and matching is case-insensitive. |
reason | string | yes | Why you're reopening the window: at least 5 characters (up to 1,000 are kept) |
reference | string | no | Your own reference, such as a ticket number (up to 200 characters are kept) |
Example
curl -X POST https://shipwave.app/api/v1/returns/window-overrides \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"orderNumber": "#1007",
"reason": "Defective unit reported after the window closed",
"reference": "Ticket 4821"
}'
Response (201 Created)
The same fields as Get Return Window, plus alreadyActive and granted. When the order already had an active override, the status is 200 OK, alreadyActive is true, and granted describes that existing override.
{
"data": {
"order": {
"id": "clx1ord2",
"orderNumber": "#1007"
},
"returnWindow": {
"returnDeadline": "2026-09-20T03:59:59.999Z",
"returnDeadlineDate": "2026-09-19",
"isOutsideWindow": true,
"daysOutsideWindow": 16
},
"activeOverride": {
"id": "clx8ovr1",
"grantedAt": "2026-10-05T16:30:00.000Z",
"expiresAt": "2026-10-20T03:59:59.999Z",
"reason": "Defective unit reported after the window closed",
"reference": "Ticket 4821"
},
"openReturn": null,
"portalUrl": "https://shipwave.app/returns/acme",
"alreadyActive": false,
"granted": {
"id": "clx8ovr1",
"grantedAt": "2026-10-05T16:30:00.000Z",
"expiresAt": "2026-10-20T03:59:59.999Z",
"days": 14
}
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is missing, isn't valid JSON, or isn't a JSON object |
| 400 | VALIDATION_ERROR | orderNumber is required |
| 400 | VALIDATION_ERROR | A reason of at least 5 characters is required |
| 404 | NOT_FOUND | No order with this number on your account (Order not found) |
| 409 | CONFLICT | The order is still inside its return window, so no override is needed. details has the same fields as Get Return Window. |
For 401, 403, and 429 responses, see Errors.