---
title: "Record bookings a person has already taken"
method: POST
path: "/api/v1/order-book/lines"
tags: ["order_book"]
---

# Record bookings a person has already taken

`POST /api/v1/order-book/lines`

## Request

- Base URL: `https://api.app.tightly.io`
- URL: `https://api.app.tightly.io/api/v1/order-book/lines`
- Auth: HTTP bearer

Record existing wholesale bookings as JSON. One booking or a whole account's season. Limit: 1 MiB; above it use file import.

IT RECORDS, IT DOES NOT PLACE. No account is contacted and no order is transmitted. On wholesale the rep is the actuator, and this is the write that says what they did.

Each line names an account, a style, a quantity and a season. `account` is matched against a trading partner's id, name or buyer reference, case-insensitively. An account this book does not hold is refused 400 `No account named {name}.`, the whole request, before anything is written. Nothing here mints an account: a misspelt name would otherwise split one retailer's book across two spellings. A key issued to one account may record that account's lines only.

Rows are versioned, not overwritten: a line whose identity (account x style x season x ship window, or your own `external_line_ref`) matches one already recorded becomes its next version when anything differs, and a no-op when it does not, so re-sending a file is safe. A claim dated before the line's current version is rejected: version order is time order.

A soft commitment (`status: hold`) may omit the price and the ship window; no other status may. Rejected lines come back in `rejected` with a reason and a sentence; the rest still records. US dollars only. 201 says a version was appended; a request that appended none answers 200.

Sold with Essentials+: without Tightly Connect the answer is 403 `plan_excludes`.

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

## Headers

- `Tightly-Version` string
- `Idempotency-Key` string

## Request body

- object
  - `known_accounts_only` boolean: Refuse, in words, a line naming an account this book does not hold, rather than creating an account for the name. It only narrows: a request made with an API key is refused unknown accounts whatever this says.
  - `lines` object[], required
    - `account` string, required: The account's id, name or buyer reference. Matched case-insensitively.
    - `booked_at` string, date-time: The day the booking was taken. Backdate to load history.
    - `cancel_date` string, date: The last day the window is open, and the day walkability is judged against.
    - `cancelled_qty` integer: Units withdrawn from this line before they shipped. Omitted means nobody has reported a withdrawal; 0 records that none were withdrawn. A line withdrawn WHOLE is `status: cancelled` instead, and the two are counted apart. Anything that is not a whole number of units is refused `invalid_cancelled_qty`.
    - `commitment_id` string: The Commitment this line belongs to, where the typing surface holds one.
    - `currency` string: USD only today.
    - `expected_version` integer: Optional concurrency guard; requires line_key and refuses if the latest version changed.
    - `external_line_ref` string: Your own identity for this line, so amendments version it rather than opening a second.
    - `line_key` string: Existing immutable line identity when amending a booking.
    - `product_id` string: The style this line is a booking of, where the caller knows it.
    - `quantity` integer, required: Units, a whole number.
    - `season` string, required: The season this booking belongs to, e.g. SS27.
    - `ship_window_start` string, date: First delivery day. Required unless the status is draft or hold.
    - `shipped_at` string, date: The day those units went out, which is what the promised window is scored against. Omitted means nobody reported a date; a quantity with no date beside it is accepted and reads as shipped undated. A date with NO quantity beside it is refused `shipped_at_without_qty`, because nothing can be reconciled from a date alone, and a date that cannot be read is refused `invalid_shipped_at`.
    - `shipped_qty` integer: Units that went out against this line. Omitted means NOBODY HAS REPORTED A SHIPMENT, which is not a shipment of nothing: send 0 to record that nothing went out. Anything that is not a whole number of units is refused `invalid_shipped_qty`.
    - `sku` string, required: The style or variant this line is for.
    - `status` string: booked, hold, draft, cancelled. Defaults to booked.
    - `wholesale_price` number: Sell-in price per unit. Required unless the status is draft or hold.

## Response `201`

At least one version was appended. The same body, with `recorded` at 0, is answered 200 when nothing was appended, every line a no-op against the version already on file, or rejected. `recorded` and `rejected` are what a caller branches on; the status says only whether the book moved.

- object
  - `data` object
    - `recorded` integer: Lines that appended a version.
    - `rejected` object[]: One entry per refused line, carrying its `failure_reason` and a sentence.
    - `status` string
    - `unchanged` integer: Lines identical to the version already on file. Not an error.
  - `message` object: severity, service and desc.

## Other responses

- `400`: An account this book does not hold, or a body that does not parse. The whole request is refused before anything is written. A misspelt account is never minted here.
- `401`: No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, telling a caller which is which maps the surface for them. A request with no Authorization header at all is refused 400 before any key is looked for.
- `403`: The key may not make this write. `scope_missing` when the key does not hold Order book at write; `plan_excludes` when Order book is sold with Essentials+ and this organisation's plan does not include it; `organization_mismatch` when the path 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. That last one is the key's own binding rather than one of the seam's five, and it is its own code so a caller can branch on it.
- `413`: Booking JSON exceeds the 1 MiB limit; use the file import for larger books.
- `429`: The key has spent its allowance for this minute. 600 reads and 120 writes a minute, counted per key, refused calls included. `Retry-After` says how long to wait.
- `503`: The database is briefly out of reach, which is what a failover or a full connection pool looks like from outside. Wait the seconds `Retry-After` names and send the request again. A read is safe to repeat. A write is not, because the connection was lost in the middle of it, so read the object back before sending it a second time. Branch on the status rather than on `code`, which is `POSTGRES_UNAVAILABLE` from one of the two seams that raise this and `SERVICE_UNAVAILABLE` from the other.

---

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