Skip to content

Returns ​

3 reads and 4 writes, on train 2026-11. Scopes: returns:read · returns:write.

OperationMethodScopePath
List returnsGETreturns:read/api/v1/sales/returns
Create returnPOSTreturns:write/api/v1/sales/returns
Get returnGETreturns:read/api/v1/sales/returns/{return_id}
Cancel returnPOSTreturns:write/api/v1/sales/returns/{return_id}/cancel
Receive returnPOSTreturns:write/api/v1/sales/returns/{return_id}/receive
Import returnsPOSTreturns:write/api/v1/sales/returns/import
Get returns summaryGETreturns:read/api/v1/sales/returns/summary

List returns ​

GET /api/v1/sales/returns

Scopes: returns:read

Every return, newest first, with what is expected back and what has arrived. state_numbers counts the WHOLE filtered book per state, not the page you were served, so a heading built on it ties to the rows underneath. Filter by state, origin, order or creation date.

OptionalInWhat it is
statequeryReturns in this state only.
originqueryReturns that came in by this door only.
order_idqueryReturns against this order only.
sincequeryReturns created on or after it.
offsetqueryRows to skip before the page starts.
limitqueryRows on the page.
Tightly-VersionheaderThe date train to answer on.
bash
curl "https://api.app.tightly.io/api/v1/sales/returns" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY"

What it answers

json
{
  "data": {
    "filtered_max_size": 3,
    "rows": [
      {
        "created_at": "2026-09-05T09:00:00+00:00",
        "expected_on": "2026-09-12",
        "id": 33,
        "location": {
          "location_id": "loc-lb",
          "name": "Long Beach"
        },
        "name": "RET-00000033",
        "order": {
          "name": "ORD-00000412",
          "order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD"
        },
        "origin": "manual",
        "received_at": null,
        "refund": {
          "cents": null,
          "currency": null,
          "reason": "No refund is recorded on this return",
          "usd": null
        },
        "refund_status": "none",
        "state": "expected",
        "units_expected": 1,
        "units_received": 0
      }
    ],
    "state_numbers": {
      "closed": 2,
      "expected": 1
    }
  },
  "message": {
    "desc": "",
    "service": "sales",
    "severity": "INFO"
    …
  }
}

What it refuses

  • 400 A filter names a value that is not one of its own (state, origin), or a date that could not be read.
  • 401 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot read Returns." The key does not hold Returns (scope_missing), names another organisation, or is outside its allowlist.
  • 429

Try it in the reference

Create return ​

POST /api/v1/sales/returns

Scopes: returns:write

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.

RequiredInWhat it is
Idempotency-KeyheaderA key you choose, up to 255 characters; Tightly keeps it 30 days.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sales/returns" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What to send, as body.json

json
{
  "expected_on": "2026-09-12",
  "lines": [
    {
      "expected_quantity": 1,
      "order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1"
    }
  ],
  "location_id": "loc-lb",
  "order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
  "origin": "manual",
  "reason": "wrong_size",
  "refund": {
    "status": "none"
  }
}

What it answers

json
{
  "data": {
    "closed_at": null,
    "exceptions": [],
    "expected_on": "2026-09-12",
    "id": 33,
    "ledger_written_at": null,
    "lines": [
      {
        "disposition": null,
        "expected_quantity": 1,
        "id": 71,
        "order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
        "received_quantity": 0,
        "refund_line_item_id": null,
        "restocked_quantity": 0,
        "sku": "MAR-TOP-S",
        "variant_id": "fx-v-mar-top-s"
      }
    ],
    "location": {
      "location_id": "loc-lb",
      "name": "Long Beach"
    },
    "movements": [],
    "name": "RET-00000033",
    "order": {
      "name": "ORD-00000412",
      "order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD"
    },
    "origin": "manual",
    "reason": "wrong_size",
    "received_at": null,
    "refund": {
      "cents": null,
      "currency": null,
      "reason": "No refund is recorded on this return",
      "usd": null
    },
    "refund_status": "none"
    …
  }
}

What it refuses

  • 400 return_needs_an_order_or_a_sku: "A return names the order it comes back from, or at least one product; this one names neither." 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 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot write Returns." 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: "ORD-00000412 shipped 2 of MAR-TOP-S and 1 came back; a return of 2 is more than is out there. Record what arrived as an exception if the warehouse counted it." 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

Try it in the reference

Get return ​

GET /api/v1/sales/returns/{return_id}

Scopes: returns:read

One return with everything on it: its lines, the ledger rows it wrote, and anything about it a person still has to decide. Call it after a receipt only if you did not read the receipt's own answer, which is this same shape.

RequiredInWhat it is
return_idpathThe return's own id.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
bash
curl "https://api.app.tightly.io/api/v1/sales/returns/<return_id>" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY"

What it answers

json
{
  "data": {
    "closed_at": "2026-09-05T09:00:00+00:00",
    "exceptions": [],
    "external_id": "gid://shopify/Refund/9912",
    "id": 31,
    "ledger_written_at": "2026-09-05T09:00:04+00:00",
    "lines": [
      {
        "disposition": "restock",
        "expected_quantity": 2,
        "id": 68,
        "order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
        "received_quantity": 2,
        "refund_line_item_id": "gid://shopify/RefundLineItem/551",
        "restocked_quantity": 2,
        "sku": "MAR-TOP-M",
        "variant_id": "fx-v-mar-top-m"
      }
    ],
    "location": {
      "location_id": "loc-lb",
      "name": "Long Beach"
    },
    "movements": [
      {
        "cost_basis": "left_at",
        "id": 4471,
        "kind": "return",
        "location_id": "loc-lb",
        "location_name": "Long Beach",
        "occurred_at": "2026-09-05T09:00:00+00:00",
        "quantity_delta": 2,
        "sequence": 1,
        "sku": "FX-MAR-TOP-M",
        "unit_cost": {
          "cents": 1200,
          "currency": "USD",
          "reason": null,
          "usd": 12
          …
        }
      }
    ]
  }
}

What it refuses

  • 401 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot read Returns." The key does not hold Returns (scope_missing), names another organisation, or is outside its allowlist.
  • 404 return_not_on_file: "No return with that id is on file." No return with that id is on file (return_not_on_file).
  • 429

Try it in the reference

Cancel return ​

POST /api/v1/sales/returns/{return_id}/cancel

Scopes: returns:write

Calls off a return nobody has received: the units stop counting as inbound and the document is finished. From expected only; a return a warehouse has already received is refused with the date it arrived, because the goods are in the building whatever anybody decides afterwards. Cancelling a return that is already cancelled answers the same document again and refuses nothing.

RequiredInWhat it is
return_idpathThe return's own id.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sales/returns/<return_id>/cancel" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY"

What it answers

json
{
  "data": {
    "closed_at": null,
    "exceptions": [],
    "id": 33,
    "ledger_written_at": null,
    "lines": [],
    "movements": [],
    "name": "RET-00000033",
    "origin": "manual",
    "received_at": null,
    "refund": {
      "cents": null,
      "currency": null,
      "reason": "No refund is recorded on this return",
      "usd": null
    },
    "refund_status": "none",
    "source_system": "tightly",
    "state": "cancelled",
    "units_expected": 1,
    "units_received": 0,
    "units_restocked": 0
  },
  "message": {
    "desc": "",
    "service": "sales",
    "severity": "INFO"
  }
}

What it refuses

  • 401 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot write Returns." The key does not hold Returns (scope_missing), names another organisation, or is outside its allowlist.
  • 404 No return with that id is on file (return_not_on_file).
  • 409 return_already_received: "RET-00000031 was received on Sep 5, 2026; nothing on it can change." return_already_received: the return has been received and nothing on it can change.
  • 429

Try it in the reference

Receive return ​

POST /api/v1/sales/returns/{return_id}/receive

Scopes: returns:write

The one act on a return that moves stock. Say what arrived at the dock and how much of it went back on the shelf; the rest came back and moved nothing sellable, which is what disposition is for. Tightly writes the ledger for the restocked units the order can admit, at the cost those units left at, and closes the return when every line is in.

RequiredInWhat it is
return_idpathThe return's own id.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sales/returns/<return_id>/receive" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What to send, as body.json

json
{
  "lines": [
    {
      "disposition": "restock",
      "received_quantity": 1,
      "restocked_quantity": 1,
      "sku": "MAR-TOP-S"
    }
  ],
  "received_at": "2026-09-12T17:00:00Z"
}

What it answers

json
{
  "data": {
    "closed_at": "2026-09-12T17:00:00+00:00",
    "exceptions": [],
    "id": 33,
    "ledger_written_at": "2026-09-12T17:00:01+00:00",
    "lines": [
      {
        "disposition": "restock",
        "expected_quantity": 1,
        "id": 71,
        "order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
        "received_quantity": 1,
        "refund_line_item_id": null,
        "restocked_quantity": 1,
        "sku": "MAR-TOP-S",
        "variant_id": "fx-v-mar-top-s"
      }
    ],
    "location": {
      "location_id": "loc-lb",
      "name": "Long Beach"
    },
    "movements": [
      {
        "cost_basis": "left_at",
        "id": 4488,
        "kind": "return",
        "location_id": "loc-lb",
        "location_name": "Long Beach",
        "occurred_at": "2026-09-12T17:00:00+00:00",
        "quantity_delta": 1,
        "sequence": 1,
        "sku": "FX-MAR-TOP-M",
        "unit_cost": {
          "cents": 1200,
          "currency": "USD",
          "reason": null,
          "usd": 12
        }
        …
      }
    ]
  }
}

What it refuses

  • 400 restocked_beyond_received: "MAR-TOP-S arrived 1 unit and 2 went back on the shelf; the shelf cannot take more than arrived." A line names no product Tightly knows (variant_unknown); a line records a negative quantity (quantity_not_positive); more went on the shelf than arrived (restocked_beyond_received); received_at could not be read (date_unreadable).
  • 401 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot write Returns." The key does not hold Returns (scope_missing), names another organisation, or is outside its allowlist.
  • 404 No return with that id is on file (return_not_on_file).
  • 409 return_already_received: "RET-00000033 was received on Sep 12, 2026; nothing on it can change." return_already_received: the return is closed and nothing on it can change; return_needs_a_place: you keep more than one warehouse and nobody named which.
  • 429

Try it in the reference

Import returns ​

POST /api/v1/sales/returns/import

Scopes: returns:write

Turns a file you have already mapped into returns expected back: one document per order number, one line per SKU. Idempotent per order number, so a corrected file updates the same returns rather than minting a second set. Upload the file first and confirm its column mapping, then send the key and the mapping here.

OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sales/returns/import" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What to send, as body.json

json
{
  "mappings": {
    "Order": "order_number",
    "Qty": "quantity",
    "Reason": "reason",
    "Style": "sku"
  },
  "s3_key": "uploads/org-1/returns-2026-09.csv"
}

What it answers

json
{
  "data": {
    "counts": {
      "line": 19,
      "return": 12
    },
    "failed_rows": [
      {
        "reason": "sku_not_resolved",
        "row": 7,
        "words": "No product with that SKU is on file."
      }
    ],
    "upload_id": "uploads/org-1/returns-2026-09.csv"
  },
  "message": {
    "desc": "",
    "service": "sales",
    "severity": "INFO"
  }
}

What it refuses

  • 400 No s3_key, or the order number, SKU and quantity columns are not all mapped. Row-level refusals are not 400s: they come back in failed_rows as missing_required_fields, invalid_quantity, order_not_found, sku_not_resolved, variant_not_on_order, return_more_than_sold, return_already_received (the file names a return the warehouse has already received; a file updates a promise, never a receipt), location_unknown or date_unreadable.
  • 401 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot write Returns." The key does not hold Returns (scope_missing), names another organisation, or is outside its allowlist.
  • 409 return_before_shipment: "Nothing on ORD-00000412 has shipped, so nothing can come back from it yet; cancel or change the order instead." A refusal the whole file cannot get past, in one of §4.3's sentences with its code.
  • 429

Try it in the reference

Get returns summary ​

GET /api/v1/sales/returns/summary

Scopes: returns:read

The return rate over the last N days, and both figures it is made of, so nothing you build has to compute it. rate_points is returned_units / fulfilled_units over the same window, IN POINTS: 2.4 means 2.4%. Where nothing shipped in that window rate_points is null with a reason beside it, because a return rate over no shipments is not 0%.

OptionalInWhat it is
daysqueryHow many days back the rate is struck over, ending today.
Tightly-VersionheaderThe date train to answer on.
bash
curl "https://api.app.tightly.io/api/v1/sales/returns/summary" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY"

What it answers

json
{
  "data": {
    "as_of": "2026-09-05T12:00:00+00:00",
    "days": 30,
    "fulfilled_units": 420,
    "rate_points": 0.7143,
    "rate_reason": null,
    "returned_units": 3,
    "returns_opened": 3,
    "since": "2026-08-06T12:00:00+00:00"
  },
  "message": {
    "desc": "",
    "service": "sales",
    "severity": "INFO"
  }
}

What it refuses

  • 400 days is outside 1 to 365.
  • 401 key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped.
  • 403 scope_missing: "This key cannot read Returns." The key does not hold Returns (scope_missing), names another organisation, or is outside its allowlist.
  • 429

Try it in the reference

Tightly API, version 2026-11.