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
StatusMeaningHow a return gets there
pendingRequested and waiting for your decisionCreate Return, or the shopper submits it in your returns portal
approvedApproved, no label yetApprove Return
deniedRejectedDeny Return
label_createdA return label was purchasedCreate Return Label
shippedThe shopper shipped the packageSet automatically for returns synced from a connected marketplace
receivedThe package arrivedMark Return Received, or automatically when the carrier reports a ShipWave return label delivered
refundedThe refund was issuedRefund Return
no_action_neededClosed with a $0 refundRefund Return when nothing is owed after fees
expiredClosed with no refund because the return label went unusedCan 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.
cancelledClosed with no refundShipWave dashboard
exchangedResolved with an exchange instead of a refundShipWave dashboard
invoice_created, invoice_sentThe shopper was billed for an item that wasn't sent backShipWave 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.

FieldTypeDescription
idstringReturn ID
orderNumberstringOrder number the return was opened against
orderDatestring | nullISO 8601 order date (Get Return only)
customerEmailstringShopper's email address
customerNamestring | nullShopper's name
statusstringSee Return Lifecycle
reasonstringReturn reason. The returns portal uses wrong_item, defective, not_as_described, changed_mind, and other.
reasonDetailstring | nullThe shopper's explanation
itemsarrayItems being returned, as they were submitted, each with a resolved price (below)
refundAmountnumber | nullAmount refunded. null until the return is refunded or closed.
shippingFeeChargednumberReturn shipping fee deducted (or to be deducted) from the refund
restockingFeeChargednumberRestocking fee deducted from the refund. 0 until the refund is processed.
returnTrackingNumberstring | nullTracking number of the return label
returnCarrierstring | nullCarrier of the return label
returnLabelUrlstring | nullURL of the return label (Get Return only)
adminNotesstring | nullInternal 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)
denialReasonstring | nullReason given when the return was denied (Get Return only)
requestedAtstringISO 8601 time the return was requested
approvedAt, deniedAt, shippedAt, receivedAt, refundedAtstring | nullISO 8601 time of each lifecycle step
labelCreatedAtstring | nullISO 8601 time the return label was purchased (Get Return only)
orderobject | nullThe linked order (Get Return only). null if the return isn't linked to an order.
returnShipmentobject | nullThe return label's shipment (Get Return only). null before a label exists.

Item object:

FieldTypeDescription
lineItemIdstringOrder line item ID, when it was provided
skustringSKU, when it was provided
namestringItem name, when it was provided
quantityintegerQuantity being returned
pricenumberUnit 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/meta envelope. The page of returns is in returns, the paging info is in pagination, and the per-status totals are in counts. The page and limit parameters work as described in Pagination.

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number (minimum 1)
limitinteger50Items per page (1–100; larger values are reduced to 100)
statusstring—Only returns with this exact status. all or no value returns every status. An unknown status returns an empty list.
searchstring—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

FieldTypeRequiredDescription
orderNumberstringyesThe 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.
customerEmailstringyesShopper's email address (up to 320 characters)
customerNamestringyesShopper's name (up to 200 characters)
reasonstringyesReturn reason, for example defective (up to 500 characters). The returns portal uses wrong_item, defective, not_as_described, changed_mind, and other.
reasonDetailstringnoThe shopper's explanation (up to 5,000 characters)
itemsarraynoItems being returned, up to 100 (defaults to an empty list). See Item fields below.
itemConditionstringnounopened or opened. Used only when your return policy charges different restocking fees for unopened and opened items; otherwise it's stored as null.
returnWindowOverrideobjectnoNeeded only when the order is past its return window and has no active override from Reopen Return Window. See below.
adminNotesstringnoA first internal note (up to 4,000 characters), saved as a timestamped Note: line

Item fields:

FieldTypeRequiredDescription
lineItemIdstringone of these threeThe order line's id. It must be a line on this order.
skustringone of these threeSKU
namestringone of these threeItem name
quantityintegernoWhole number from 1 to 10,000 (default 1)
pricenumbernoUnit price, 0 or more. Leave it out to use the price the shopper paid for the matching order line.
reasonstringnoReason for this item (up to 500 characters)

Other item fields are dropped. Text fields can be up to 500 characters.

returnWindowOverride object:

FieldTypeRequiredDescription
confirmedbooleanyesMust be true
reasonstringyesWhy you're accepting a late return (up to 1,000 characters are kept)
referencestringnoYour 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 with 404 NOT_FOUND and no return is created.
  • Later steps depend on the items. Create Return Label needs each item's sku to 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 timeZone shown in the response (currently America/New_York for every store). A return requested after that needs either an active override from Reopen Return Window or a confirmed returnWindowOverride; otherwise the request fails with 409 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
    }
  }
}
FieldDescription
itemsThe items as stored: the fields listed above, trimmed, with quantity filled in. Prices are resolved in Get Return.
restockingFeePercentRestocking fee rate quoted for this return. With a condition-tiered policy, a return created without itemCondition is quoted at the opened rate.
returnWindow.deliveredAtDelivery date the window started from, or null when it started from the order date
returnWindow.returnDeadlineLast moment a return is on time (ISO 8601)
returnWindow.returnDeadlineDateLast on-time calendar day, YYYY-MM-DD in timeZone
returnWindow.daysOutsideWindowCalendar days the request is past the deadline (0 when on time)
returnWindow.exceptionAppliedtrue when the order was past its window and was accepted because of your returnWindowOverride or an active window override

Errors

StatusCodeCause
400VALIDATION_ERRORThe body is missing, isn't valid JSON, or isn't a JSON object
400VALIDATION_ERRORA 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
400VALIDATION_ERRORAn 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.
400VALIDATION_ERRORitemCondition must be "unopened" or "opened", or returnWindowOverride isn't an object
404NOT_FOUNDNo order with this number on your account (Order not found)
409CONFLICTorderNumber matches more than one of your orders and none exactly. Send it exactly as the Orders API returns it. details.orderNumbers lists the matches.
409CONFLICTThe 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

StatusCodeCause
404NOT_FOUNDNo 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

FieldTypeRequiredDescription
adminNotesstringnoThe 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

StatusCodeCause
400VALIDATION_ERRORThe body is missing, isn't valid JSON, or isn't a JSON object
400VALIDATION_ERRORThe body includes refundAmount (refundAmount is read-only. Process refunds through POST /api/v1/returns/:id/refund.)
400VALIDATION_ERRORadminNotes isn't a string, or is longer than 4,000 characters
404NOT_FOUNDNo return with this ID on your account (Return request not found)
409CONFLICTThe 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.

FieldTypeDescription
adminNotesstringA note to add to the return's internal notes (see Add Return Note)
returnWindowOverrideobject{ "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

StatusCodeCause
400VALIDATION_ERRORThe body isn't valid JSON or isn't a JSON object
400VALIDATION_ERRORThe body includes refundAmount (refundAmount is read-only. Approval does not set the eventual refund amount.)
400VALIDATION_ERRORadminNotes isn't a string or is too long, or returnWindowOverride isn't an object
400VALIDATION_ERRORThe return isn't pending (Cannot approve return with status: approved)
404NOT_FOUNDNo return with this ID on your account (Return request not found)
409CONFLICTThe 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.
409CONFLICTAnother 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

FieldTypeRequiredDescription
denialReasonstringyesWhy the return was denied (up to 2,000 characters)
adminNotesstringnoA 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

StatusCodeCause
400VALIDATION_ERRORThe body is missing, isn't valid JSON, or isn't a JSON object
400VALIDATION_ERRORdenialReason 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.
400VALIDATION_ERRORadminNotes isn't a string or is too long
400VALIDATION_ERRORThe return isn't pending (Cannot deny return with status: approved)
404NOT_FOUNDNo return with this ID on your account (Return request not found)
409CONFLICTAnother 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: customerAddress if you send it. Otherwise the ship-to address of the order's first shipment, with the return's customerName. If your customerAddress has no phone, 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 with idempotent: true, and nothing is bought. labelPdfUrl and labelPngUrl are null and allRates is 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 with 409 CONFLICT and 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.

FieldTypeDescription
customerAddressobjectAddress the shopper ships from (below)
parcelWeightnumberParcel weight in ounces
parcelLengthnumberParcel length in inches
parcelWidthnumberParcel width in inches
parcelHeightnumberParcel height in inches
replaceExistingLabelbooleantrue 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:

FieldTypeRequired
namestringno
street1stringyes
street2stringno
citystringyes
statestringyes
zipstringyes
countrystringno (defaults to US)
phonestringno

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
  }
}
FieldDescription
label.labelUrlLabel to send to the shopper
label.labelPdfUrlThe label as a PDF, when the carrier provided one. Otherwise null.
label.labelPngUrlThe label as a PNG (the same file as labelUrl when it is a PNG). Otherwise null.
label.shippingFeeReturn 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.customerPaysReturnShippingtrue when the shopper bears return shipping: your policy doesn't offer free returns, or the return is international
label.originalShippingRefundableAlways false. Refunds cover the returned items and their tax, not the original shipping charge.
label.isInternationaltrue when the shopper's country differs from your warehouse's
label.returnedGoodsTreatmentFor an international return of goods originally shipped from the US, the returned-goods customs declaration ShipWave filed. Otherwise null.
label.customsFormUrl, customsFormType, customsFormSubmittedElectronicallyThe commercial invoice for an international return, when the carrier produced one. null for domestic returns.
allRatesThe 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).
idempotenttrue when the return already had a label and this response returns it without buying another
replacedLabelWith replaceExistingLabel: true, the label that was voided: trackingNumber and the carrier's voidStatus. Otherwise null.

Errors

StatusCodeCause
400VALIDATION_ERRORThe 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
400VALIDATION_ERRORThe return isn't approved or label_created (Cannot create label for return with status: pending)
400VALIDATION_ERRORNo warehouse configured for returns: none of your warehouses has a complete address
400VALIDATION_ERRORCustomer address not found. Please provide address manually.: no customerAddress and no shipping address on the order
400VALIDATION_ERRORReturn request is not linked to an order
400VALIDATION_ERRORA returned item has no weight on file, or a product that needs catalog dimensions has none
400VALIDATION_ERRORCannot generate label. Missing weight data.
400VALIDATION_ERRORNo shipping rates available for this return
400VALIDATION_ERRORFedEx 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)
400VALIDATION_ERRORThe shopper has no phone number and your return warehouse address has none to use instead. Add a phone to either and try again.
400VALIDATION_ERRORAn international return is missing customs data. The message lists what's missing, for example an item's HS code or country of origin.
400VALIDATION_ERRORThe carrier rejected the shipment or the purchase, for example an address it can't verify. The message includes the carrier's reason.
404NOT_FOUNDNo return with this ID on your account (Return request not found)
409CONFLICTThe return shares one label with another return. Manage that label in the ShipWave dashboard.
409CONFLICTAnother label request for this return is in progress, or an earlier purchase didn't finish. Fetch the return before trying again.
409CONFLICTreplaceExistingLabel was sent but the current label couldn't be voided. Nothing was bought.
502INTERNAL_ERRORThe 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.

FieldTypeDescription
adminNotesstringA 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

StatusCodeCause
400VALIDATION_ERRORThe body isn't valid JSON or isn't a JSON object, or adminNotes isn't a string or is too long
400VALIDATION_ERRORThe return isn't label_created or shipped (Cannot mark as received with status: approved)
404NOT_FOUNDNo return with this ID on your account (Return request not found)
409CONFLICTThe 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 get 409 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.

FieldTypeDescription
adminNotesstringA note to add to the return's internal notes (up to 4,000 characters), before the refund entry ShipWave adds
feeOverrideobjectChanges the restocking fee, the return shipping fee, or both (below)
inspectionSkipobjectConfirms refunding without a recorded inspection (below). Needed only when a restocking fee applies.

Bodies that include finalRefundAmount, refundAmount, or amount are rejected.

feeOverride object:

FieldTypeRequiredDescription
confirmedbooleanyesMust be true
reasonstringyesWhy you're changing the fees (up to 1,000 characters are kept)
restockingFeenumberone of these twoRestocking fee to deduct, in dollars (0 or more)
shippingFeenumberone of these twoReturn 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:

FieldTypeRequiredDescription
confirmedbooleanyesMust be true
reasonstringyesWhy 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
  }
}
FieldDescription
statusrefunded, or no_action_needed when the refund is $0
refundedAtISO 8601 refund time. null for no_action_needed.
itemsSubtotalReturned items at the price paid
itemsTaxSales tax on the returned items
defaultShippingFee, defaultRestockingFeeFees ShipWave would deduct without a feeOverride
shippingFeeDeducted, restockingFeeDeductedFees actually deducted
finalRefundAmountAmount refunded to the shopper
feeOverrideAppliedtrue when your feeOverride was used
inspectionSkippedtrue when the refund went ahead without a recorded inspection
shopifyRefundIdID of the refund on the Shopify order. Omitted when no Shopify refund was made.
refundReceiptEmailSentWhether the receipt email reached the mail server
dutyRefundFor 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.
idempotenttrue 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

StatusCodeCause
400VALIDATION_ERRORThe body isn't valid JSON or isn't a JSON object, or includes finalRefundAmount, refundAmount, or amount
400VALIDATION_ERRORadminNotes isn't a string or is longer than 4,000 characters
400VALIDATION_ERRORfeeOverride or inspectionSkip isn't an object, a fee isn't a number of 0 or more, or feeOverride changes neither fee
400VALIDATION_ERRORThe fees add up to more than the returned items plus tax, or an international return's shipping fee is below the label cost
404NOT_FOUNDNo return with this ID on your account (Return request not found)
409CONFLICTThe return isn't received (Cannot refund return with status: label_created. Must be "received" first.)
409CONFLICTfeeOverride or inspectionSkip is missing confirmed: true or a reason
409CONFLICTA restocking fee applies, no inspection is recorded, and there's no inspectionSkip
409CONFLICTThe return already has an exchange or replacement order and can't also be refunded
409CONFLICTThe calculated refund is more than the order's remaining refundable balance (after earlier refunds and lost chargebacks)
409CONFLICTAn earlier refund attempt for this return is still in progress, or another request is retrying it
409CONFLICTAn earlier attempt failed and Shopify shows a refund on the order that ShipWave can't match to it. Nothing was refunded. Contact ShipWave support.
409CONFLICTShopify issued the refund but ShipWave couldn't finish recording it. Don't retry; contact ShipWave support.
409CONFLICTA return-first exchange has no price difference owed to the shopper, or a feeOverride was sent for one
409CONFLICTA Walmart return isn't linked to its channel, or the channel is disconnected
422VALIDATION_ERRORThe 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
422VALIDATION_ERRORSales tax couldn't be confirmed with Shopify, so the refund was held
422VALIDATION_ERRORA refund above $0 needs a linked Shopify order, and the return has none (or the store connection is incomplete)
422VALIDATION_ERRORA returned item couldn't be matched to a Shopify order line, so nothing was refunded
422VALIDATION_ERRORShopify rejected the refund (Shopify refund failed: ...)
502INTERNAL_ERRORWalmart rejected the refund, or ShipWave couldn't reach Shopify to check an earlier failed attempt. Nothing was refunded.
500INTERNAL_ERRORUnexpected 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

ParameterTypeRequiredDescription
orderNumberstringyesOrder 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"
  }
}
FieldDescription
returnWindowThe order's window as of now, calculated as in Create Return. null if the order has no dates to start from.
activeOverrideThe 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.
portalUrlYour returns portal, or null if you haven't set one up

Errors

StatusCodeCause
400VALIDATION_ERRORorderNumber is required
404NOT_FOUNDNo 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 CONFLICT with the window's state in details.

Request Body

FieldTypeRequiredDescription
orderNumberstringyesOrder number. The leading # is optional and matching is case-insensitive.
reasonstringyesWhy you're reopening the window: at least 5 characters (up to 1,000 are kept)
referencestringnoYour 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

StatusCodeCause
400VALIDATION_ERRORThe body is missing, isn't valid JSON, or isn't a JSON object
400VALIDATION_ERRORorderNumber is required
400VALIDATION_ERRORA reason of at least 5 characters is required
404NOT_FOUNDNo order with this number on your account (Order not found)
409CONFLICTThe 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.