# Returns API

Customers ask for returns on your returns page in the app. The API lets your own systems see those requests and act on them. It does not create return requests: those always come from the customer, who proves an order with the order or tracking number and the email on it.

## Permissions

- `returns:read` lists and reads returns.
- `returns:write` approves, rejects, receives and closes them.

## Lifecycle

A return has one of these statuses: `requested`, `approved`, `rejected`, `label_issued`, `in_transit`, `received`, `closed` or `cancelled`. Approval buys a prepaid return label and emails it to the customer.

| Call | What it does |
| --- | --- |
| `GET /returns` | List returns. Filter with `?status=requested`. |
| `GET /returns/{id}` | Read one return. |
| `POST /returns/{id}/approve` | Approve and buy the prepaid return label. **Spends wallet money.** Accepts an optional `note`. |
| `POST /returns/{id}/reject` | Reject. A `note` is required and explains the decision. |
| `POST /returns/{id}/receive` | Mark the parcel as received. |
| `POST /returns/{id}/close` | Close a finished return. |

**Caution:** Approving a return spends wallet money for the prepaid label. It is safe to repeat: a return that already has its label is returned unchanged, and a return that was approved but could not get a label (for example the wallet was empty) tries to buy the label again.

## Example

```bash
# Find requests waiting for a decision
curl "https://shipjay.com/api/v1/returns?status=requested" \
  -H "Authorization: Bearer $SHIPJAY_KEY"

# Approve one
curl -X POST https://shipjay.com/api/v1/returns/$RETURN_ID/approve \
  -H "Authorization: Bearer $SHIPJAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Within the 30 day window" }'
```
```js
const headers = {
  Authorization: `Bearer ${process.env.SHIPJAY_KEY}`,
  "Content-Type": "application/json",
};

const list = await (await fetch("https://shipjay.com/api/v1/returns?status=requested", { headers })).json();
for (const ret of list.data) {
  // Apply your own policy here before approving.
  await fetch(`https://shipjay.com/api/v1/returns/${ret.id}/approve`, {
    method: "POST",
    headers,
    body: JSON.stringify({ note: "Within the 30 day window" }),
  });
}
```
```python
headers = {"Authorization": f"Bearer {os.environ['SHIPJAY_KEY']}"}

listing = requests.get(
    "https://shipjay.com/api/v1/returns", params={"status": "requested"}, headers=headers, timeout=30
).json()
for ret in listing["data"]:
    # Apply your own policy here before approving.
    requests.post(
        f"https://shipjay.com/api/v1/returns/{ret['id']}/approve",
        headers=headers,
        json={"note": "Within the 30 day window"},
        timeout=30,
    )
```
A return looks like this:

```json
{
  "id": "a41d9e27-6b80-4c35-8f12-d3e05b7a9c61",
  "rma": 1042,
  "status": "requested",
  "shipment_id": "2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20",
  "return_shipment_id": null,
  "reason": "too_small",
  "reason_note": null,
  "customer_email": "avery@example.com",
  "customer_name": "Avery Quinn",
  "label_error": null,
  "created_at": "2026-05-06T09:30:00Z",
  "received_at": null
}
```

`reason` is one of `too_small`, `too_large`, `damaged`, `wrong_item`, `not_as_described`, `changed_mind` or `other`. `label_error` explains why the prepaid label could not be bought. The return stays approved, and approving it again retries the label.

The list returns the 200 most recent returns that match the filter, newest first. It does not paginate, and `next_cursor` is always `null`.

## Events

Return changes produce `return.requested`, `return.approved`, `return.rejected` and `return.received` events. Use them to react to a new request without polling. See [Webhooks](/docs/webhooks).