Skip to main content

REST API

ReturnDesk Pro has a REST API under the namespace tsre/v1, and three WooCommerce webhook topics. Staff routes manage returns, customer routes work on the signed-in customer's own returns, and two routes manage store credit.

Pro feature

The REST API and the webhook topics are part of the Pro edition. The Lite edition registers only the signed photo route GET /photos/{id} (see "Photos" below), which the return screen uses to show the customer's photos. Every other route answers 404 rest_no_route in Lite.

Base address: https://example.com/wp-json/tsre/v1

Authenticationโ€‹

RoutesWho can call them
/returns...A signed-in user with the manage_tsre_returns capability (Administrator and Shop manager)
/settingsA signed-in user with manage_woocommerce
/me/returns...Any signed-in user. A customer sees only their own returns, other ids answer 404.
/credits/... (Pro)A signed-in user with manage_tsre_returns
/photos/{id}A signed link, see "Photos" below
/webhooks/{carrier} (Pro)The carrier service, authenticated by signature, see Labels and carriers

ReturnDesk uses the standard WordPress REST authentication:

  • Application Passwords for scripts and other servers. Create one on the user profile and send it with HTTP Basic authentication over HTTPS.
  • Cookie sessions for code running in the browser. Send the X-WP-Nonce header with the wp_rest nonce.
curl -u "api-user:YOUR_APPLICATION_PASSWORD" \
"https://example.com/wp-json/tsre/v1/returns?status[]=requested&per_page=20"

Without a valid login, staff routes answer 401 (or 403 when you are signed in without the capability) with rest_forbidden, and /me routes answer 401 with rest_not_logged_in. The API has no rate limit of its own.

Staff routesโ€‹

List returnsโ€‹

GET /returns

ParameterTypeNotes
statusarrayOne or more of requested, approved, label_sent, in_transit, received, inspected, resolved, rejected, cancelled
return_methodarrayOne or more of prepaid_label, customer_label, dropoff, in_store, keep
searchstringUp to 200 characters. Matches the RMA number and the email, and the order id when numeric.
customer_id, order_idintegerMinimum 1
date_from, date_tostringYYYY-MM-DD or YYYY-MM-DD HH:MM:SS, on the creation date. A date alone covers the whole day.
pageintegerDefault 1
per_pageinteger1 to 100, default 20
orderbystringcreated_at (default), updated_at, status, return_number, order_id
orderstringasc or desc (default)

The body is an array of returns. The headers X-WP-Total and X-WP-TotalPages give the totals.

Get a returnโ€‹

GET /returns/{id}

Returns the return with its items, its events (the timeline) and its photos.

{
"id": 39,
"return_number": "RMA-2026-000039",
"order_id": 74,
"order_number": "74",
"customer_id": 3,
"customer_email": "[email protected]",
"status": "requested",
"resolution": "refund",
"return_method": "customer_label",
"currency": "EUR",
"refund_total": 35.0,
"credit_total": 0.0,
"fee_total": 0.0,
"exchange_order_id": null,
"refund_id": null,
"label_id": null,
"reason_summary": "Wrong size or fit",
"customer_note": "The sleeves are a little too short for me.",
"admin_note": null,
"window_expires_at": "2026-11-27 18:40:00",
"created_at": "2026-10-28 16:40:00",
"updated_at": "2026-10-28 16:40:00",
"resolved_at": null,
"erased_at": null,
"items": [
{
"id": 61,
"return_id": 39,
"order_item_id": 12,
"product_id": 22,
"variation_id": 0,
"qty_requested": 1,
"qty_accepted": 0,
"qty_rejected": 0,
"reason_code": "wrong_size",
"reason_text": "",
"condition_grade": null,
"restock": false,
"unit_refund": 35.0,
"line_refund": 0.0
}
],
"events": [],
"photos": []
}

Dates are UTC. The guest access token is never part of the response.

Create a returnโ€‹

POST /returns starts a return for a customer's order, as the signed-in member of staff. The same eligibility rules apply as for the customer, so an item outside its window is refused.

FieldTypeNotes
order_idintegerRequired
itemsarrayRequired, 1 to 100 lines. Each line is an object with order_item_id and qty (both required, minimum 1), and optionally reason_code, reason_text (up to 500 characters), exchange_product_id and exchange_variation_id (Pro).
resolutionstringRequired: refund, credit, exchange or warranty
return_methodstringRequired: prepaid_label, customer_label, dropoff, in_store or keep
notestringUp to 2000 characters

Any other field is rejected with 400. The answer is 201 with the new return.

curl -u "api-user:YOUR_APPLICATION_PASSWORD" -X POST \
-H "Content-Type: application/json" \
-d '{"order_id":74,"items":[{"order_item_id":12,"qty":1,"reason_code":"wrong_size"}],"resolution":"refund","return_method":"customer_label"}' \
"https://example.com/wp-json/tsre/v1/returns"

Move a return to a statusโ€‹

POST /returns/{id}/transition

FieldTypeNotes
tostringRequired: approved, rejected, in_transit, received or cancelled
notestringUp to 2000 characters. The internal note of an approval, or the reason of a rejection.
tracking_numberstringUp to 100 characters, used with in_transit

label_sent, inspected and resolved have their own flows (labels, the inspect route and the resolve route). A move the status machine does not allow answers 409 with tsre_invalid_transition. See Customer flow for the allowed moves.

Inspect a returnโ€‹

POST /returns/{id}/inspect records the inspection of a received return.

FieldTypeNotes
linesarrayRequired. One object per returned item: order_item_id, qty_accepted and condition_grade are required, qty_rejected (default 0) and restock (boolean) are optional.

condition_grade is new, opened, used or damaged. Unknown properties are rejected, and an item can only be listed once. Accepted plus rejected cannot exceed the requested quantity.

Resolve a returnโ€‹

POST /returns/{id}/resolve resolves an inspected return. The plan is rebuilt on the server, so the request carries no amounts to trust.

FieldTypeNotes
amountnumberA lower amount to give back instead of the whole plan, only on a refund or a store credit. An exchange or warranty claim answers 400 tsre_amount_not_allowed.
recoverystringaccept, discard or retry: a decision about a refund that failed before, see Admin flow
confirmbooleanMust be true for a recovery decision, and to resolve a return that the customer keeps

A recovery decision only works after a recorded failure of the matching kind. A resolution that fails answers 409 tsre_resolve_failed with the message.

Read the settingsโ€‹

GET /settings needs manage_woocommerce and returns all settings except the secrets.

{
"settings": { "window_days": 30, "window_from": "completed", "eu_withdrawal": true },
"configured": { "easypost_api_key": false, "easypost_webhook_secret": false },
"pro": true
}

configured tells for each secret whether a value is set.

Customer routesโ€‹

These routes act on the signed-in user's own returns.

RouteWhat it does
GET /me/returnsLists the customer's returns. Parameters: status (array), page, per_page (1 to 100).
GET /me/returns/{id}One return. The customer's view leaves out the internal note, the customer id and email and the refund ids, and adds status_label, can_cancel, can_ship, can_note, label_pending, instructions, return_address, pay_url, the timeline, the labels and the photos.
POST /me/returnsStarts a return for one of the customer's orders. Same fields and limits as the staff route. Other people's orders answer 404.
POST /me/returns/{id}/cancelCancels the return, while it is Requested, Approved or Label sent.
POST /me/returns/{id}/shippedMarks the return as on its way. Optional tracking_number, up to 100 characters.

POST /me/returns takes no photos. Photos are uploaded in the My Account wizard, not through the REST API, so a reason that requires a photo cannot be used through this route.

The customer routes never accept the recovery decisions of the resolve route.

Credits (Pro)โ€‹

RouteWhat it does
GET /credits/{customer_id}The balance and the ledger, newest first. limit is 1 to 200, default 50.
POST /credits/{customer_id}Adds store credit.

POST fields: amount (required, greater than 0, in the store currency), note (up to 500 characters) and expires_at (YYYY-MM-DD HH:MM:SS in UTC; no expiry when left out).

Credits can only be added. There is no route to remove credit, edit an entry or set a balance. The answer is 201 with the balance and the ledger.

{
"customer_id": 3,
"currency": "EUR",
"balance": 20.0,
"ledger": [
{
"id": 7,
"amount": 20.0,
"balance_after": 20.0,
"source_type": "manual",
"source_id": 0,
"expires_at": null,
"note": "Goodwill credit",
"created_at": "2026-10-28 16:55:00"
}
]
}

Photosโ€‹

GET /photos/{id}?exp={timestamp}&sig={signature} serves a return photo. The photo links inside the photos list of a staff or customer response are signed, expire after 15 minutes and carry a _wpnonce parameter.

The signature is not enough on its own. A link only opens in a browser where a signed-in user who may see the photo (staff, or the owner of the return) has a cookie session. In other words, the photo links in the REST view need a cookie session: pasting one into another tool, or opening it while signed out, answers 403 tsre_forbidden. To see photos from an external system, open the return screen in WooCommerce > Returns while you are signed in.

Limitsโ€‹

FieldLimit
Items per return request100
reason_text of an item500 characters
note of a return request, note of a transition2000 characters
tracking_number100 characters
search200 characters
per_page100
Ledger limit200
Credit note500 characters

Errorsโ€‹

Errors use the WordPress format:

{ "code": "tsre_invalid_transition", "message": "This action is not available for the return's current state.", "data": { "status": 409 } }
CodeStatusMeaning
rest_forbidden401, 403Not allowed
rest_not_logged_in401/me route without a login
rest_invalid_param400A parameter failed validation, or an unknown field was sent
tsre_refused400The request breaks a rule, for example an item outside its window. The message says which.
tsre_not_found404The return, order or customer does not exist or is not yours
tsre_claim_held409Another request for the same order is being saved
tsre_invalid_transition409The return cannot move to that status
tsre_resolve_failed409The resolution did not finish
tsre_pro_required400The option needs the Pro edition
tsre_amount_not_allowed400The amount can only change on a refund or a store credit
tsre_error500Unexpected error. The details are in the WooCommerce logs.

Webhooksโ€‹

ReturnDesk registers three topics for WooCommerce webhooks. Add them under WooCommerce > Settings > Advanced > Webhooks.

TopicFires when
tsre_return.created (Return created)A return is created
tsre_return.updated (Return updated)A return changes status
tsre_return.resolved (Return resolved)A return is resolved

The payload is the same object as the staff list entry: the return fields and its items, without the guest token and without the internal note. Events and photos are not part of it. The payload is built when the delivery runs, so it shows the state at that moment. Deliveries are signed by WooCommerce with the X-WC-Webhook-Signature header.

note

WooCommerce delivers a webhook at most once per return per request. If one request changes the status of a return several times, the Return updated webhook is delivered once and shows the final state. Created, updated and resolved are separate webhooks, so each of them still fires.