---
title: "create return"
method: POST
path: "/api/v1/sales/returns"
tags: ["returns"]
---

# create return

`POST /api/v1/sales/returns`

## Request

- Base URL: `https://api.app.tightly.io`
- URL: `https://api.app.tightly.io/api/v1/sales/returns`
- Auth: HTTP bearer

Records one return: the order the goods come back from, the products and how many of each, where they are expected and why. The return is EXPECTED until a warehouse receives it, and an expected return moves no stock: it counts as inbound on the position read and nothing else. Send an `Idempotency-Key` header; the same key with the same body answers the same return, the same key with a different body is refused, and Tightly keeps a key 30 days.

Returns that come from a connected channel are never created here: Tightly reads the channel's refund on the sync and builds the return from it. The refund on a return is the channel's fact too, so a refund sent for an order the channel owns is refused.

Scope: `returns:write`, which includes `returns:read`.

## Headers

- `Idempotency-Key` string, required
- `Tightly-Version` string

## Request body

- object
  - `exchange_order_id` string, nullable
  - `expected_on` string, date, nullable: YYYY-MM-DD.
  - `lines` object[], required
    - `expected_quantity` integer, required
    - `order_line_item_id` string, nullable
    - `sku` string, nullable
    - `variant_id` string, nullable
  - `location_id` string, nullable: Where it is expected back. Absent, Tightly reads the order's own warehouse, then your single active one; an organisation with several and nothing said is refused.
  - `order_id` string, nullable: The order the goods come back from.
  - `origin` 'manual' | 'api' | 'csv' | 'portal': Which door this return came in by. `channel` is not among them.
  - `reason` 'wrong_size' | 'damaged' | 'not_as_described' | 'changed_mind' | 'late' | 'other', nullable
  - `reason_note` string, nullable
  - `refund` object, nullable: What the money did. A fact you record, never one Tightly issues.
    - `amount_cents` integer, nullable
    - `currency` string, nullable
    - `refunded_at` string, date-time, nullable
    - `status` 'none' | 'partial' | 'full'
    - `timing` 'before_receipt' | 'after_receipt', nullable

## Response `201`

The return as recorded, expected back.

- object
  - `data` object: The return, in the shape GET /sales/returns/{return_id} serves.
  - `message` object: severity, service and desc.

## Other responses

- `400`: A line names no product or no positive quantity (`quantity_not_positive`, `variant_unknown`); a line names a product the order does not carry (`variant_not_on_order`); the return names neither an order nor a product (`return_needs_an_order_or_a_sku`); a partial or full refund carries no amount (`refund_needs_an_amount`); a date is not written YYYY-MM-DD (`date_unreadable`); no Idempotency-Key was sent (`idempotency_key_required`).
- `401`: No usable key: malformed, unknown, revoked, expired or stopped.
- `403`: The key may not reach this operation. `scope_missing`: the key does not hold Returns; `organization_mismatch`: the request names an organisation that is not the key's; `ip_not_allowed`: the caller's address is outside the key's allowlist.
- `404`: No order with that id is on file (`order_not_on_file`).
- `409`: `return_more_than_sold`: more is coming back than the order still has out there; `return_before_shipment`: nothing on the order has shipped; `refund_is_the_channels`: the refund on this order is recorded in the channel; `return_needs_a_place`: you keep more than one warehouse and nobody named which; `idempotency_key_reused`: the key was already used for a different body.
- `429`: The key has spent its allowance for this minute. 600 reads and 120 writes a minute, counted per key, refused calls included. `Retry-After` says how long to wait.
- `503`: The database is briefly out of reach, which is what a failover or a full connection pool looks like from outside. Wait the seconds `Retry-After` names and send the request again. A read is safe to repeat. A write is not, because the connection was lost in the middle of it, so read the object back before sending it a second time. Branch on the status rather than on `code`, which is `POSTGRES_UNAVAILABLE` from one of the two seams that raise this and `SERVICE_UNAVAILABLE` from the other.

---

[API](https://skmtc.dev/tightly/apis/tightly-api.md) · [All operations](https://skmtc.dev/tightly/apis/tightly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tightly/apis/tightly-api/revisions/199f7eb35afb?raw)
