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.
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โ
| Routes | Who can call them |
|---|---|
/returns... | A signed-in user with the manage_tsre_returns capability (Administrator and Shop manager) |
/settings | A 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-Nonceheader with thewp_restnonce.
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
| Parameter | Type | Notes |
|---|---|---|
status | array | One or more of requested, approved, label_sent, in_transit, received, inspected, resolved, rejected, cancelled |
return_method | array | One or more of prepaid_label, customer_label, dropoff, in_store, keep |
search | string | Up to 200 characters. Matches the RMA number and the email, and the order id when numeric. |
customer_id, order_id | integer | Minimum 1 |
date_from, date_to | string | YYYY-MM-DD or YYYY-MM-DD HH:MM:SS, on the creation date. A date alone covers the whole day. |
page | integer | Default 1 |
per_page | integer | 1 to 100, default 20 |
orderby | string | created_at (default), updated_at, status, return_number, order_id |
order | string | asc 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.
| Field | Type | Notes |
|---|---|---|
order_id | integer | Required |
items | array | Required, 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). |
resolution | string | Required: refund, credit, exchange or warranty |
return_method | string | Required: prepaid_label, customer_label, dropoff, in_store or keep |
note | string | Up 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
| Field | Type | Notes |
|---|---|---|
to | string | Required: approved, rejected, in_transit, received or cancelled |
note | string | Up to 2000 characters. The internal note of an approval, or the reason of a rejection. |
tracking_number | string | Up 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.
| Field | Type | Notes |
|---|---|---|
lines | array | Required. 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.
| Field | Type | Notes |
|---|---|---|
amount | number | A 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. |
recovery | string | accept, discard or retry: a decision about a refund that failed before, see Admin flow |
confirm | boolean | Must 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.
| Route | What it does |
|---|---|
GET /me/returns | Lists 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/returns | Starts 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}/cancel | Cancels the return, while it is Requested, Approved or Label sent. |
POST /me/returns/{id}/shipped | Marks 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)โ
| Route | What 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โ
| Field | Limit |
|---|---|
| Items per return request | 100 |
reason_text of an item | 500 characters |
note of a return request, note of a transition | 2000 characters |
tracking_number | 100 characters |
search | 200 characters |
per_page | 100 |
Ledger limit | 200 |
Credit note | 500 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 } }
| Code | Status | Meaning |
|---|---|---|
rest_forbidden | 401, 403 | Not allowed |
rest_not_logged_in | 401 | /me route without a login |
rest_invalid_param | 400 | A parameter failed validation, or an unknown field was sent |
tsre_refused | 400 | The request breaks a rule, for example an item outside its window. The message says which. |
tsre_not_found | 404 | The return, order or customer does not exist or is not yours |
tsre_claim_held | 409 | Another request for the same order is being saved |
tsre_invalid_transition | 409 | The return cannot move to that status |
tsre_resolve_failed | 409 | The resolution did not finish |
tsre_pro_required | 400 | The option needs the Pro edition |
tsre_amount_not_allowed | 400 | The amount can only change on a refund or a store credit |
tsre_error | 500 | Unexpected error. The details are in the WooCommerce logs. |
Webhooksโ
ReturnDesk registers three topics for WooCommerce webhooks. Add them under WooCommerce > Settings > Advanced > Webhooks.
| Topic | Fires 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.
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.