Skip to content

Orders ​

8 reads and 10 writes, on train 2026-11. Scopes: orders:read · orders:write.

OperationMethodScopePath
List customersGETorders:read/api/v1/sales/customers
Get customerGETorders:read/api/v1/sales/customers/{customer_id}
List invoicesGETorders:read/api/v1/sales/invoices
Get an invoiceGETorders:read/api/v1/sales/invoices/{invoice_id}
List sales ordersGETorders:read/api/v1/sales/orders
Create sales orderPOSTorders:write/api/v1/sales/orders
Get sales orderGETorders:read/api/v1/sales/orders/{order_id}
Update sales orderPATCHorders:write/api/v1/sales/orders/{order_id}
Allocate sales orderPOSTorders:write/api/v1/sales/orders/{order_id}/allocate
Cancel sales orderPOSTorders:write/api/v1/sales/orders/{order_id}/cancel
Confirm sales orderPOSTorders:write/api/v1/sales/orders/{order_id}/confirm
Invoice a sales orderPOSTorders:write/api/v1/sales/orders/{order_id}/invoice
Send shipping noticePOSTorders:write/api/v1/sales/orders/{order_id}/shipping-notice
Every order file that has arrivedGETorders:read/api/v1/sales/orders/documents
File one order a retailer sentPOSTorders:write/api/v1/sales/orders/documents
One order file, its reading and the source to check it againstGETorders:read/api/v1/sales/orders/documents/{document_id}
Take a refused order file as the new draftPOSTorders:write/api/v1/sales/orders/documents/{document_id}/take
Import sales ordersPOSTorders: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.

OptionalInWhat it is
trading_partner_idqueryOnly this account's own customer record.
searchqueryA term containing an @ is read as an email address and matched against its pseudonym, exactly.
offsetqueryHow many customers to skip. The list is ordered by the most recent order, newest first.
limitqueryHow many customers to return, at most 100.
Tightly-VersionheaderThe 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 offset or limit is 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

Try it in the reference

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.

RequiredInWhat it is
customer_idpathThe customer's id, as List customers and an order's customer block serve it.
OptionalInWhat it is
Tightly-VersionheaderThe 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

Try it in the reference

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.

OptionalInWhat it is
trading_partner_idqueryOnly this account's invoices.
statequeryThe document's own state, which is not the ledger's and not the network's.
order_idqueryOnly the invoices raised against this order.
offsetqueryHow many invoices to skip, for the page after this one.
limitqueryHow many invoices one page carries, capped where every list is capped.
Tightly-VersionheaderThe 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

Try it in the reference

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.

RequiredInWhat it is
invoice_idpathThe invoice's id.
OptionalInWhat it is
Tightly-VersionheaderThe 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

Try it in the reference

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.

OptionalInWhat it is
lifecyclequeryOnly orders in this state.
needs_youqueryOnly orders somebody has to answer for: a wholesale draft nobody has confirmed, or an order carrying an open queue row.
originqueryOnly orders that came in by this door.
is_wholesalequeryTrue for orders an account placed, false for orders a shopper placed.
trading_partner_idqueryOnly orders for this account.
sales_channel_idqueryOnly orders on this channel.
searchqueryMatches an order's name, its reference, or the account it is for.
sincequeryOnly orders placed on or after this day.
offsetqueryHow many orders to skip, for paging.
limitqueryHow many orders to return.
Tightly-VersionheaderThe 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 since is 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

Try it in the reference

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.

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/orders" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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_missing when the key does not hold Orders; organization_mismatch when the request names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist; account_mismatch when 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_reused when the key was already used for a different body; idempotency_key_in_flight when the first attempt on this key is still running, and the response carries Retry-After.
  • 429

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's id.
OptionalInWhat it is
Tightly-VersionheaderThe 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

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's 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 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.json

What 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

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's 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/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

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's 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/orders/<order_id>/cancel" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's 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/orders/<order_id>/confirm" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's 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/orders/<order_id>/invoice" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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

Try it in the reference

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.

RequiredInWhat it is
order_idpathThe order's 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/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

Try it in the reference

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.

OptionalInWhat it is
statequeryreceived is not read yet; applied made a draft; refused created nothing and says why.
lanequeryThe 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_idqueryOne account's papers only.
sincequeryPapers received on or after this day.
limitqueryRows per page.
offsetqueryRows to skip, for the next page.
Tightly-VersionheaderThe 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_excludes when this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+; scope_missing when the key does not hold Orders; ip_not_allowed when the caller's address is outside the key's allowlist; account_mismatch when a key issued to one account names another, or a paper that is not that account's.
  • 429

Try it in the reference

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.

RequiredInWhat it is
account_idqueryThe account this order is for. Required; never read off the paper.
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/orders/documents?account_id=<account_id>" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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_excludes when this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+; scope_missing when the key does not hold Orders; ip_not_allowed when the caller's address is outside the key's allowlist; account_mismatch when 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

Try it in the reference

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.

RequiredInWhat it is
document_idpathThe order file's id, as List order documents serves it.
OptionalInWhat it is
Tightly-VersionheaderThe 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_excludes when this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+; scope_missing when the key does not hold Orders; ip_not_allowed when the caller's address is outside the key's allowlist; account_mismatch when 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

Try it in the reference

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.

RequiredInWhat it is
document_idpathThe order file's id, as List order documents serves it.
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/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_excludes when this organisation's plan does not include Tightly Connect, which the arriving order file needs and which is sold with Essentials+; scope_missing when the key does not hold Orders; ip_not_allowed when the caller's address is outside the key's allowlist; account_mismatch when 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

Try it in the reference

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.

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/orders/import" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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

Try it in the reference

Tightly API, version 2026-11.