Appearance
Orders
8 reads and 10 writes, on train 2026-11. Scopes: orders:read · orders:write.
| Operation | Method | Scope | Path |
|---|---|---|---|
| List customers | GET | orders:read | /api/v1/sales/customers |
| Get customer | GET | orders:read | /api/v1/sales/customers/{customer_id} |
| List invoices | GET | orders:read | /api/v1/sales/invoices |
| Get an invoice | GET | orders:read | /api/v1/sales/invoices/{invoice_id} |
| List sales orders | GET | orders:read | /api/v1/sales/orders |
| Create sales order | POST | orders:write | /api/v1/sales/orders |
| Get sales order | GET | orders:read | /api/v1/sales/orders/{order_id} |
| Update sales order | PATCH | orders:write | /api/v1/sales/orders/{order_id} |
| Allocate sales order | POST | orders:write | /api/v1/sales/orders/{order_id}/allocate |
| Cancel sales order | POST | orders:write | /api/v1/sales/orders/{order_id}/cancel |
| Confirm sales order | POST | orders:write | /api/v1/sales/orders/{order_id}/confirm |
| Invoice a sales order | POST | orders:write | /api/v1/sales/orders/{order_id}/invoice |
| Send shipping notice | POST | orders:write | /api/v1/sales/orders/{order_id}/shipping-notice |
| Every order file that has arrived | GET | orders:read | /api/v1/sales/orders/documents |
| File one order a retailer sent | POST | orders:write | /api/v1/sales/orders/documents |
| One order file, its reading and the source to check it against | GET | orders:read | /api/v1/sales/orders/documents/{document_id} |
| Take a refused order file as the new draft | POST | orders:write | /api/v1/sales/orders/documents/{document_id}/take |
| Import sales orders | POST | orders:write | /api/v1/sales/orders/import |
List customers
GET /api/v1/sales/customers
Scopes: orders:read
Everyone the record knows, from a connected channel or from an order somebody typed. Deliberately thin: a display name, the email's domain, a country and a coarsened region, and never a street address, a phone number or an email in the clear. Do not call this to build a marketing list, and do not call it to find one person you already have the id for: GET /sales/customers/{customer_id} serves that one with their orders and returns.
| Optional | In | What it is |
|---|---|---|
trading_partner_id | query | Only this account's own customer record. |
search | query | A term containing an @ is read as an email address and matched against its pseudonym, exactly. |
offset | query | How many customers to skip. The list is ordered by the most recent order, newest first. |
limit | query | How many customers to return, at most 100. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/customers" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 2,
"offset": 0,
"rows": [
{
"account": {
"name": "Coastline DS",
"trading_partner_id": "tp-coastline"
},
"country_code": "US",
"customer_id": "cus_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"display_name": "Coastline DS",
"display_name_words": "Coastline DS",
"email_domain": "coastline.example",
"first_order_at": "Jul 14, 2026",
"kind": "account",
"last_order_at": "Sep 5, 2026",
"orders_count": 3,
"redacted_at": null,
"region": "CA",
"source_system": "tightly"
},
{
"account": null,
"country_code": "US",
"customer_id": "cus_7QK2V0X9MB3D5T8HAJ1NCR6FZ4",
"display_name": null,
"display_name_words": "Not on file",
"email_domain": null,
"first_order_at": "Sep 2, 2026",
"kind": "consumer",
"last_order_at": "Sep 2, 2026",
"orders_count": 1,
"redacted_at": "Sep 4, 2026",
"region": "NY",
"source_system": "shopify"
}
],
"size": 2
…
}
}What it refuses
- 400
offsetorlimitis outside the range the list offers. - 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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and named another (account_mismatch). - 429
Get customer
GET /api/v1/sales/customers/{customer_id}
Scopes: orders:read
The person, their orders in the same row shape the Orders list serves, their returns, and what has been refunded to them across every channel they bought on. Call it to answer a question about a buyer; do not call it in a loop over a list, which is what the list itself is for. There is no write: a customer is created by the order writers and by the sync, never by this API.
| Required | In | What it is |
|---|---|---|
customer_id | path | The customer's id, as List customers and an order's customer block serve it. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/customers/<customer_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"account": {
"name": "Coastline DS",
"trading_partner_id": "tp-coastline"
},
"country_code": "US",
"customer_id": "cus_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"display_name": "Coastline DS",
"display_name_words": "Coastline DS",
"email_domain": "coastline.example",
"first_order_at": "Sep 5, 2026",
"kind": "account",
"last_order_at": "Sep 5, 2026",
"orders": [
{
"account": {
"name": "Coastline DS",
"trading_partner_id": "tp-coastline"
},
"channel": {
"name": "Wholesale",
"sales_channel_id": "wholesale"
},
"commitment_id": null,
"created_at": "2026-09-05T09:00:00+00:00",
"customer": {
"customer_id": "cus_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"display_name": "Coastline DS"
},
"exceptions_open": 0,
"fulfilled_units": 0,
"is_wholesale": true,
"lifecycle": "allocated",
"lifecycle_updated_at": "2026-09-05T09:12:00+00:00",
"location": {
"location_id": "loc-lb",
"name": "Long Beach"
},
"name": "ORD-00000412"
…
}
]
}
}What it refuses
- 400 The path carries no customer id.
- 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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and the record is not that account's (account_mismatch). - 404
customer_not_on_file: "No customer with that id is on file." No customer with that id is on file (customer_not_on_file). - 429
List invoices
GET /api/v1/sales/invoices
Scopes: orders:read
The wholesale invoices Tightly has raised, newest first, with what the books did with each and what the network did with its 810. Use it to reconcile your accounts receivable against what shipped. It does not include consumer orders: a storefront's own payment is not an invoice Tightly raises.
| Optional | In | What it is |
|---|---|---|
trading_partner_id | query | Only this account's invoices. |
state | query | The document's own state, which is not the ledger's and not the network's. |
order_id | query | Only the invoices raised against this order. |
offset | query | How many invoices to skip, for the page after this one. |
limit | query | How many invoices one page carries, capped where every list is capped. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/invoices" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 1,
"max_size": 50,
"offset": 0,
"rows": [
{
"account": {
"name": "Tidewater Surf Co.",
"trading_partner_id": "fx-tp-tidewater"
},
"currency": "USD",
"edi": {
"document_id": 45,
"sent_at": "2026-09-06T17:30:01+00:00",
"sentence": null,
"state": "sent"
},
"invoice_id": 4,
"issued_at": "2026-09-06T17:30:00+00:00",
"ledger": {
"external_url": "https://go.xero.com/AccountsReceivable/Edit.aspx?InvoiceID=8d1f0f1c",
"ledger": "xero",
"posted_at": "2026-09-06T17:30:02+00:00",
"sentence": null,
"state": "posted"
},
"name": "INV-00000004",
"order": {
"name": "TIDE-5581",
"order_id": "fx-ord-tide-5581"
},
"state": "issued",
"terms": "net_30",
"total": {
"cents": 780000,
"currency": "USD",
"reason": null,
"usd": 7800
}
…
}
]
}
}What it refuses
- 400 An offset or limit outside the bounds every list keeps.
- 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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and named another (account_mismatch). - 429
Get an invoice
GET /api/v1/sales/invoices/{invoice_id}
Scopes: orders:read
One wholesale invoice: what was billed, at what price, against which order and shipment, what your books did with it and what the network did with its 810. Use it to answer an account's query about a document they hold.
| Required | In | What it is |
|---|---|---|
invoice_id | path | The invoice's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/invoices/<invoice_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"account": {
"name": "Tidewater Surf Co.",
"trading_partner_id": "fx-tp-tidewater"
},
"created_by": "usr_01J8V3H2Q9",
"currency": "USD",
"edi": {
"document_id": 45,
"sent_at": "2026-09-06T17:30:01+00:00",
"sentence": null,
"state": "sent"
},
"freight": {
"cents": 0,
"currency": "USD",
"reason": null,
"usd": 0
},
"invoice_id": 4,
"issued_at": "2026-09-06T17:30:00+00:00",
"ledger": {
"external_id": "8d1f0f1c-0f2a-4f3e-9a11-6c2f5f0e7a10",
"external_url": "https://go.xero.com/AccountsReceivable/Edit.aspx?InvoiceID=8d1f0f1c",
"ledger": "xero",
"posted_at": "2026-09-06T17:30:02+00:00",
"sentence": null,
"state": "posted"
},
"lines": [
{
"amount": {
"cents": 468000,
"currency": "USD",
"reason": null,
"usd": 4680
},
"buyer_sku": "4472-S",
"line_number": 1
…
}
]
}
}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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and the record is not that account's (account_mismatch). - 404
invoice_not_on_file: "No invoice with that id is on file." No invoice with that id is on file. - 429
List sales orders
GET /api/v1/sales/orders
Scopes: orders:read
Every order Tightly holds, whatever it came in by: a connected channel, a person, a file, this API, an EDI 850 or the buyer portal. status_numbers counts the whole filtered book per lifecycle, not the page, so a total shown above the rows can be relied on. Do not call this to find one order you already have the id for: GET /sales/orders/{order_id} serves that one whole, and this list deliberately carries no address.
| Optional | In | What it is |
|---|---|---|
lifecycle | query | Only orders in this state. |
needs_you | query | Only orders somebody has to answer for: a wholesale draft nobody has confirmed, or an order carrying an open queue row. |
origin | query | Only orders that came in by this door. |
is_wholesale | query | True for orders an account placed, false for orders a shopper placed. |
trading_partner_id | query | Only orders for this account. |
sales_channel_id | query | Only orders on this channel. |
search | query | Matches an order's name, its reference, or the account it is for. |
since | query | Only orders placed on or after this day. |
offset | query | How many orders to skip, for paging. |
limit | query | How many orders to return. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/orders" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"as_of": "2026-09-05T12:00:00+00:00",
"filtered_max_size": 6,
"max_size": 6,
"offset": 0,
"rows": [
{
"account": {
"name": "Coastline DS",
"trading_partner_id": "tp-coastline"
},
"channel": {
"name": "Wholesale",
"sales_channel_id": "wholesale"
},
"commitment_id": null,
"created_at": "2026-09-05T09:00:00+00:00",
"customer": null,
"exceptions_open": 0,
"fulfilled_units": 0,
"is_wholesale": true,
"lifecycle": "allocated",
"lifecycle_updated_at": "2026-09-05T09:12:00+00:00",
"location": {
"location_id": "loc-lb",
"name": "Long Beach"
},
"name": "ORD-00000412",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"origin": "manual",
"units": 900,
"value": {
"cents": 3710000,
"currency": "USD",
"reason": null,
"usd": 37100
}
}
]
…
}
}What it refuses
- 400 A filter carries a value the list does not offer, or
sinceis not a date written YYYY-MM-DD (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 read Orders." The key does not hold Orders (scope_missing), or is issued to one account and named another (account_mismatch). - 429
Create sales order
POST /api/v1/sales/orders
Scopes: orders:write
Creates one order Tightly owns from the moment it exists: its lines, the channel or the account it belongs to, where it ships and what it is worth. Send an Idempotency-Key header; the same key with the same body answers the same order, the same key with a different body is refused, and a key sent while the first attempt is still running is refused with Retry-After. Tightly keeps a key 30 days.
| Required | In | What it is |
|---|---|---|
Idempotency-Key | header | A key you choose, up to 255 characters; Tightly keeps it 30 days. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl -X POST "https://api.app.tightly.io/api/v1/sales/orders" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"cancel_date": "2026-10-06",
"currency": "USD",
"external_reference": "WEB-88231",
"is_wholesale": true,
"lifecycle": "open",
"lines": [
{
"quantity": 400,
"unit_price_cents": 4400,
"variant_id": "fx-v-mar-top-m"
},
{
"quantity": 500,
"sku": "MAR-BTM-M",
"unit_price_cents": 3900
}
],
"location_id": "loc-lb",
"origin": "manual",
"requested_ship_date": "2026-09-22",
"sales_channel_id": "wholesale",
"ship_to": {
"address1": "700 Queensway Drive",
"city": "Long Beach",
"country_code": "US",
"name": "Coastline DS",
"postcode": "90802",
"region": "CA"
},
"trading_partner_id": "tp-coastline"
}What it answers
json
{
"data": {
"account": {
"name": "Coastline DS",
"trading_partner_id": "tp-coastline"
},
"channel": {
"name": "Wholesale",
"sales_channel_id": "wholesale"
},
"commitment_id": null,
"created_at": "2026-09-05T09:00:00+00:00",
"customer": null,
"exceptions_open": 0,
"fulfilled_units": 0,
"is_wholesale": true,
"lifecycle": "open",
"lifecycle_updated_at": "2026-09-05T09:00:00+00:00",
"lines": [
{
"fulfilled_quantity": 0,
"line_number": 1,
"line_total": {
"cents": 1760000,
"currency": "USD",
"reason": null,
"usd": 17600
},
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
"part_of_bundle": null,
"quantity": 400,
"reservations": [],
"returned_quantity": 0,
"sku": "MAR-TOP-M",
"unit_price": {
"cents": 4400,
"currency": "USD",
"reason": null,
"usd": 44
}
…
}
]
}
}What it refuses
- 400
variant_unknown: "MAR-BTM-M is not a product Tightly knows." A line has no product or no positive quantity (quantity_not_positive,variant_unknown); the body could not be read; no Idempotency-Key was sent (idempotency_key_required), or the one sent is longer than 255 characters (idempotency_key_too_long); no account, channel or warehouse is on file for the one named (account_unknown,channel_unknown,location_unknown); a line has no price on file, or its price on file is in another currency than the order's (price_unknown); a date is not written YYYY-MM-DD (date_unreadable). - 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five. - 403
scope_missing: "This key cannot write Orders." The key may not reach this operation.scope_missingwhen the key does not hold Orders;organization_mismatchwhen the request names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another, in a sentence naming both. - 409
idempotency_key_reused: "Idempotency-Key ord-88231 was already used for a different request; use a new key."idempotency_key_reusedwhen the key was already used for a different body;idempotency_key_in_flightwhen the first attempt on this key is still running, and the response carriesRetry-After. - 429
Get sales order
GET /api/v1/sales/orders/{order_id}
Scopes: orders:read
One order and everything hanging off it: its lines with what has been fulfilled and returned, what each line holds and where, the fulfilment requests sent to a warehouse, the shipments, returns and invoices against it, its open exceptions, and the booking it was born from. ship_to is served here and on no list or export, because an address is the document's and not the page's.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/orders/<order_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"book": null,
"edi": null,
"exceptions": [],
"exceptions_open": 0,
"financial": {
"subtotal": {
"cents": 3710000,
"currency": "USD",
"reason": null,
"usd": 37100
}
},
"fulfilled_units": 0,
"fulfilment_requests": [],
"invoices": [],
"lifecycle": "allocated",
"lines": [],
"name": "ORD-00000412",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"origin": "manual",
"returns": [],
"ship_to": {
"city": "Long Beach",
"country_code": "US",
"name": "Coastline DS"
},
"shipments": [],
"units": 900,
"withheld_words": null
},
"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 read Orders." The key does not hold Orders (scope_missing), or is issued to one account and the record is not that account's (account_mismatch). - 404
order_not_on_file: "No order with that id is on file." No order with that id is on file. - 429
Update sales order
PATCH /api/v1/sales/orders/{order_id}
Scopes: orders:write
Changes what a person may change on an order Tightly owns: its ship date, its cancel date, your own reference, the warehouse it will ship from, and its lines. lines names only the lines you are changing, each by its order_line_item_id (as the read serves it) or by variant_id or sku. A quantity of 0 removes one, a variant_id the order does not carry adds one, and a line you do not name is left alone, so two people editing one order do not delete each other's work.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X PATCH "https://api.app.tightly.io/api/v1/sales/orders/<order_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"lines": [
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
"quantity": 360
}
],
"requested_ship_date": "2026-09-29"
}What it answers
json
{
"data": {
"lifecycle": "allocated",
"name": "ORD-00000412",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"units": 860
},
"message": {
"desc": "",
"service": "sales",
"severity": "INFO"
}
}What it refuses
- 400
line_not_on_order: "MAR-BTM-M is not on ORD-00000412." A line names a product the order does not carry (line_not_on_order), a product Tightly does not know (variant_unknown), or a quantity below zero (quantity_not_positive); the warehouse named is not on file (location_unknown); a date is not written YYYY-MM-DD (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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and the record is not that account's (account_mismatch). - 404 No order with that id is on file.
- 409
order_locked_for_fulfilment: "ORD-00000412 was sent to Long Beach on Sep 5, 2026; nothing on it can change until Long Beach answers."channel_order_is_the_channels,order_locked_for_fulfilment,order_already_fulfilled,order_already_cancelled. - 429
Allocate sales order
POST /api/v1/sales/orders/{order_id}/allocate
Scopes: orders:write
Holds stock for one open order. Turning reservations on does not reach back over the orders that were open before it, so this is the act that allocates one of them. An order already allocated, sent, fulfilled or cancelled is refused with the state it is in; an organisation that does not hold stock for orders is refused with where to turn it on; and an order whose channel has no warehouse linked is refused with what to link.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/<order_id>/allocate" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"lifecycle": "allocated",
"lines": [
{
"is_backordered": false,
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
"quantity": 400,
"reservations": [
{
"location_id": "loc-lb",
"location_name": "Long Beach",
"quantity": 400,
"shortfall": 0,
"state": "held"
}
],
"sku": "MAR-TOP-M"
}
],
"location": {
"location_id": "loc-lb",
"name": "Long Beach"
},
"name": "ORD-00000412",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD"
},
"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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and the record is not that account's (account_mismatch). - 404 No order with that id is on file.
- 409
order_not_open: "ORD-00000412 is allocated; only an open order can be allocated."order_not_open,order_reservations_off,no_serving_location. - 429
Cancel sales order
POST /api/v1/sales/orders/{order_id}/cancel
Scopes: orders:write
Cancels an order Tightly owns and releases every unit it was holding, so the stock is sellable again in the same request. A reason is optional and is kept on the document.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/<order_id>/cancel" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"reason": "The account moved the season"
}What it answers
json
{
"data": {
"cancel_reason": "The account moved the season",
"lifecycle": "cancelled",
"name": "ORD-00000412",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD"
},
"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 Orders." The key does not hold Orders (scope_missing), or is issued to one account and the record is not that account's (account_mismatch). - 404 No order with that id is on file.
- 409
order_already_cancelled: "ORD-00000412 was cancelled on Sep 5, 2026."channel_order_is_the_channels,order_locked_for_fulfilment,order_already_fulfilled,order_already_cancelled. - 429
Confirm sales order
POST /api/v1/sales/orders/{order_id}/confirm
Scopes: orders:write
Records what you are agreeing to on every line of a draft order and opens it, or moves it straight to allocated where the organisation holds stock for orders. Each line is accepted as asked, accepted at a different quantity, or rejected; every line must carry one, so nothing is agreed to on your behalf. On an order that arrived over EDI the acknowledgement (855) goes back to the account where they take one, and where they do not the decision is recorded and nothing is sent. A line the account named with a code that is not on their product list has to be matched or rejected first; until it is, the confirm is refused with how many lines are waiting.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/<order_id>/confirm" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"lines": [
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
"quantity": 8000,
"reason": "the run covers 8,000",
"verdict": "quantity_changed"
},
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:2",
"verdict": "accepted"
},
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:3",
"verdict": "accepted"
},
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:4",
"verdict": "rejected"
}
]
}What it answers
json
{
"data": {
"book": {
"commitment_id": "cmt_ss27",
"line_key": null,
"season": "SS27",
"version": 1
},
"edi": {
"acknowledgement": {
"document_id": 42,
"sent_at": "2026-09-05T17:30:00+00:00",
"sentence": null,
"state": "sent"
},
"document_id": 41,
"po_number": "CDS-90114",
"state": "applied"
},
"lifecycle": "open",
"lines": [
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:1",
"quantity": 8000,
"requested_quantity": 8500,
"sku": "MAR-TOP-M",
"verdict": "quantity_changed"
},
{
"order_line_item_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD:4",
"quantity": 0,
"requested_quantity": 100,
"sku": "MAR-BTM-L",
"verdict": "rejected"
}
],
"name": "ORD-00000412",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"origin": "edi",
"units": 9400
…
}
}What it refuses
- 400
verdict_missing_on_a_line: "Line 2 of ORD-00000412 has no verdict; accept it, change its quantity or reject it before confirming." A line has no verdict (verdict_missing_on_a_line); a changed quantity is not a whole number above zero (quantity_not_positive); the body names a line the order does not have (line_not_on_order); the body 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 write Orders." The key does not hold Orders (scope_missing), or the organisation's plan does not include wholesale and the draft arrived over EDI, or the key is issued to one account and the order is another's (account_mismatch). - 404 No order with that id is on file.
- 409
draft_has_unmatched_lines: "ORD-00000412 still has 1 line with no match on Coastline Department Stores's product list; match or reject it before confirming."order_not_a_draft,draft_has_unmatched_lines,order_already_cancelled. - 429
Invoice a sales order
POST /api/v1/sales/orders/{order_id}/invoice
Scopes: orders:write
Raises the account's invoice for the quantities that were actually fulfilled, at the price the order was agreed at, and sends it as an 810 where the account takes one. It is also posted to Xero or QuickBooks as a receivable where a ledger is connected. Tax and freight are figures you send; Tightly never calculates either.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/<order_id>/invoice" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"freight_cents": 0,
"tax_cents": 0,
"terms": "net_30"
}What it answers
json
{
"data": {
"account": {
"name": "Tidewater Surf Co.",
"trading_partner_id": "fx-tp-tidewater"
},
"currency": "USD",
"edi": {
"document_id": 45,
"sent_at": "2026-09-06T17:30:01+00:00",
"sentence": null,
"state": "sent"
},
"freight": {
"cents": 0,
"currency": "USD",
"reason": null,
"usd": 0
},
"invoice_id": 4,
"issued_at": "2026-09-06T17:30:00+00:00",
"ledger": {
"external_id": "8d1f0f1c-0f2a-4f3e-9a11-6c2f5f0e7a10",
"external_url": "https://go.xero.com/AccountsReceivable/Edit.aspx?InvoiceID=8d1f0f1c-0f2a-4f3e-9a11-6c2f5f0e7a10",
"ledger": "xero",
"posted_at": "2026-09-06T17:30:02+00:00",
"sentence": null,
"state": "posted"
},
"lines": [
{
"amount": {
"cents": 468000,
"currency": "USD",
"reason": null,
"usd": 4680
},
"buyer_sku": "4472-S",
"line_number": 1,
"order_line_item_id": "fx-ord-tide-5581:1"
…
}
]
}
}What it refuses
- 400
terms_not_known: "terms takes net_30, net_60, net_90, due_on_receipt; net_45 is not one of them."terms_not_known,tax_cents_not_whole_cents,freight_cents_not_whole_cents,tax_cents_negative,freight_cents_negative, or a body 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 write Orders." The key does not hold Orders (scope_missing), or the organisation's plan does not include wholesale, or the key is issued to one account and the order is another's (account_mismatch). - 404 No order with that id is on file.
- 409
invoice_already_issued: "TIDE-5581 was invoiced on Sep 6, 2026 as INV-00000004; void it before issuing another."order_is_not_an_accounts,nothing_fulfilled_to_invoice,invoice_already_issued,line_without_a_price(a fulfilled line with no price on it and no sell-in price on the account; nothing is invoiced at $0.00). - 429
Send shipping notice
POST /api/v1/sales/orders/{order_id}/shipping-notice
Scopes: orders:write
Sends the account an 856 shipping notice built from what the warehouse actually shipped: the cartons, the barcode on each, the carrier and the tracking number. Call it once the warehouse has confirmed the shipment. It is refused where the warehouse reported no cartons, because a notice with a guessed pack structure is what a retailer receives against and charges you for; type the carton list against the shipment first.
| Required | In | What it is |
|---|---|---|
order_id | path | The order's id. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/<order_id>/shipping-notice" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"fulfilled_units": 200,
"is_wholesale": true,
"lifecycle": "fulfilled",
"name": "TIDE-5581",
"order_id": "fx-ord-tide-5581",
"origin": "manual",
"shipments": [
{
"carrier": "UPSN",
"carton_count": 2,
"cartons": [
{
"carton_number": 1,
"sscc": "00000123456789012345",
"units": 120
},
{
"carton_number": 2,
"sscc": "00000123456789012352",
"units": 80
}
],
"name": "SHP-00000007",
"shipment_id": 7,
"shipping_notice": {
"document_id": 44,
"sent_at": "2026-09-06T17:30:00+00:00",
"sentence": null,
"state": "sent"
},
"tracking_number": "1Z999AA10123456784",
"units": 200
}
],
"units": 200
},
"message": {
"desc": "A shipping notice was sent to Tidewater Surf Co."
…
}
}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 Orders." The key does not hold Orders (scope_missing), or the organisation's plan does not include wholesale, or the key is issued to one account and the order is another's (account_mismatch). - 404 No order with that id is on file.
- 409
shipment_without_cartons: "Long Beach reported no cartons for TIDE-5581, so no shipping notice was sent; type the carton list against the shipment or ask Long Beach for it."order_is_not_an_accounts(the order belongs to no account),partner_takes_no_856(the account does not take one, or nobody has asked),nothing_shipped_to_notify,shipment_without_cartons,cartons_exceed_what_shipped,partner_not_connected. - 429
Every order file that has arrived
GET /api/v1/sales/orders/documents
Scopes: orders:read
Every purchase order a retailer has sent you, whichever door it came through: a mail attachment, a followed link, an upload, a buyer's own send through their seat, or an 850 over EDI. Each row says what state the paper is in, what Tightly read off it, and which draft order it became.
| Optional | In | What it is |
|---|---|---|
state | query | received is not read yet; applied made a draft; refused created nothing and says why. |
lane | query | The door the paper came through: an upload, a mail attachment, a link in a mail, the buyer's own seat, or the EDI network. |
trading_partner_id | query | One account's papers only. |
since | query | Papers received on or after this day. |
limit | query | Rows per page. |
offset | query | Rows to skip, for the next page. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/orders/documents" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 2,
"rows": [
{
"account": {
"name": "Juniper & Fable",
"trading_partner_id": "tp-juniper-fable"
},
"document_id": 41,
"file_name": "order-88231.pdf",
"lane": "upload",
"needs_verification": true,
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BD",
"order_name": "ORD-00000413",
"po_number": "88231",
"read_word": "Read from pages 1 and 2",
"received_at": "Sep 5, 2026",
"refusal": null,
"state": "applied",
"type": "pdf",
"units": 186,
"verification_words": [
"1 new code"
],
"verified_at": null,
"verified_by": null,
"words": [
"Totals tie"
]
}
],
"state_numbers": {
"applied": 1,
"refused": 1
}
},
"message": {
"desc": "",
"service": "inventory"
…
}
}What it refuses
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five. - 403
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+;scope_missingwhen the key does not hold Orders;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another, or a paper that is not that account's. - 429
File one order a retailer sent
POST /api/v1/sales/orders/documents
Scopes: orders:write
Files one purchase order a retailer sent you, as a PDF, a CSV, a workbook or a photographed page, against the account you name. Upload the file first with the presigned upload and send its key here. Tightly reads the paper on a worker and makes one draft sales order from it, with a line per garment resolved through that account's own codes; the answer comes back immediately with the paper in received, and the draft appears on it when the read lands.
| Required | In | What it is |
|---|---|---|
account_id | query | The account this order is for. Required; never read off the paper. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/documents?account_id=<account_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"file_name": "order-88231.pdf",
"mime_type": "application/pdf",
"s3_key": "5f2c/u_88/2f2a1c94-0f61-4c1b-9a2e-6d0b5c47e0a1_order-88231.pdf"
}What it answers
json
{
"data": {
"account": {
"name": "Juniper & Fable",
"trading_partner_id": "tp-juniper-fable"
},
"document_id": 41,
"facts": {},
"file_name": "order-88231.pdf",
"header_match": {},
"lane": "upload",
"lines": [],
"matched": [],
"needs_verification": true,
"order_id": null,
"order_name": null,
"po_number": null,
"read_word": "Not read",
"reading": {},
"received_at": "Sep 5, 2026",
"refusal": null,
"source": {
"page_count": null,
"pages_read": [],
"type": "pdf",
"url": "https://uploads.example/…"
},
"state": "received",
"template_version_id": null,
"type": "pdf",
"units": null,
"unmatched_lines": 0,
"verification_words": [],
"verified_at": null,
"verified_by": null,
"words": []
},
"message": {
"desc": "",
"service": "inventory"
…
}
}What it refuses
- 400
account_unknown: "Name the account this order is for; Tightly never reads it off the paper." No account was named (account_unknown); the file is larger than 20 MB or is a kind Tightly does not read (file_not_readable); the key is not one of this organisation's uploads. - 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five. - 403
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+;scope_missingwhen the key does not hold Orders;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another, or a paper that is not that account's. - 404 No account with that id is on file.
- 409
edi_document_duplicate: "This file was already received on Sep 3, 2026 as Juniper & Fable's order 88231; nothing was created twice."edi_document_duplicate, these bytes were already received;file_names_another_account, the paper names a different account you have on file. - 429
One order file, its reading and the source to check it against
GET /api/v1/sales/orders/documents/{document_id}
Scopes: orders:read
One purchase order as it arrived, and what Tightly read off it: the header facts, a line per garment with the codes as printed, and where on the paper each figure was read from, a page and the passage cited on it or a sheet row and column. source.url is a short lived link to the paper itself, so a person can stand the extracted lines beside it.
| Required | In | What it is |
|---|---|---|
document_id | path | The order file's id, as List order documents serves it. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/orders/documents/<document_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"account": {
"name": "Juniper & Fable",
"trading_partner_id": "tp-juniper-fable"
},
"document_id": 41,
"facts": {
"po_number": {
"read_word": "Read from page 1",
"span": {
"cited_text": "Order No. 88231",
"kind": "page",
"page": 1
},
"value": "88231"
}
},
"file_name": "order-88231.pdf",
"header_match": {
"currency": "USD",
"currency_differs": false,
"door_word": null,
"payment_terms": "Net 30",
"trading_partner_warehouse_id": 7
},
"lane": "upload",
"lines": [
{
"buyer_sku": "JF-4471-BLK-M",
"document_line": 1,
"line_number": 1,
"quantity": 12,
"read_word": "Read from page 1",
"size": "M",
"span": {
"cited_text": "JF-4471-BLK M 12",
"kind": "page",
"page": 1
}
…
}
]
}
}What it refuses
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five. - 403
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+;scope_missingwhen the key does not hold Orders;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another, or a paper that is not that account's. - 404
document_not_on_file: "No order file with that id is on file." No order file with that id is on file (document_not_on_file). - 429
Take a refused order file as the new draft
POST /api/v1/sales/orders/documents/{document_id}/take
Scopes: orders:write
Takes a paper Tightly refused and makes it the order instead. Use it when a retailer sends the same order number again with different lines: the first draft still stands, and this cancels it with the reason and reads the newer paper in its place, in one call.
| Required | In | What it is |
|---|---|---|
document_id | path | The order file's id, as List order documents serves it. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/documents/<document_id>/take" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"document_id": 42,
"lane": "upload",
"order_id": "ord_01JQ8ZK7C4N2R9V6T3M0X5A1BE",
"order_name": "ORD-00000414",
"po_number": "88231",
"read_word": "Read from page 1",
"state": "applied",
"type": "pdf",
"units": 198,
"unmatched_lines": 0
},
"message": {
"desc": "",
"service": "inventory",
"severity": "INFO"
}
}What it refuses
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five. - 403
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+;scope_missingwhen the key does not hold Orders;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another, or a paper that is not that account's. - 404 No order file with that id is on file (
document_not_on_file). - 409
document_still_reading: "Order 88231 is still being read; open it in a moment."document_still_reading, the paper has not been read yet;document_not_refused, it was not refused, so there is nothing to take;document_replaces_nothing, it was refused for something other than a standing draft;standing_order_confirmed, the order it would replace is no longer a draft;file_names_another_account, the paper's letterhead names another account on file. - 429
Import sales orders
POST /api/v1/sales/orders/import
Scopes: orders:write
Turns a file already uploaded and column-mapped into owned orders: one document per order number and channel, one line per SKU. Re-importing the same file updates the same orders rather than making a second set, so a corrected file is sent again rather than cleaned up by hand. Rows that cannot be written come back in failed_rows with the reason in words, and nothing in the file is written twice.
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A 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/orders/import" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"mappings": {
"account": "Customer",
"channel": "Channel",
"order_number": "PO Number",
"quantity": "Qty",
"requested_ship_date": "Ship date",
"sku": "Style",
"unit_price": "Price"
},
"s3_key": "64f/9a1/orders-aw26.csv"
}What it answers
json
{
"data": {
"counts": {
"line": 148,
"order": 12
},
"failed_rows": [
{
"failure_reason": "sku_not_resolved",
"failure_reason_words": "No product with that SKU is on file.",
"order_number": "PO-8841",
"quantity": "4",
"sku": "MAR-BTM-XS"
}
],
"upload_id": "64f/9a1/orders-aw26.csv"
},
"message": {
"desc": "",
"service": "sales",
"severity": "INFO"
}
}What it refuses
- 400 The body names no file, or maps none of the order number, the SKU and the quantity.
- 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 Orders." The key does not hold Orders (scope_missing), or is issued to one account, which a file of orders cannot be narrowed to (account_mismatch). - 429