Docs / Reference

API reference

The public REST API, included in every plan.

The OrderDen public API is a REST API over what the app does: clients, the item catalog, quotes, orders and shipments, invoices and payments, vendors, purchases, spreadsheet imports, the activity trail, dashboard data, search and settings. It's included in every plan, Free included, with no monthly quota, only a per-minute rate per key (see Plans & billing). New features ship with API coverage.

Try it live: the interactive API explorer shows every endpoint with its parameters and schemas. Click Authorize, paste an API key, and run requests against your own workspace. The OpenAPI specification behind it also works with Postman, code generators and other OpenAPI tools.

The API runs the same code as the app, so validation, derived statuses, document numbering and the walls between workspaces are the same either way.

Authentication

Create a key in Company Settings → API & integrations (owners and admins). A workspace can have up to 10 active keys. The secret, od_live_ followed by 40 hex characters, is shown once; only its hash is stored. Send it as a bearer token on every request:

curl -s https://your-domain.example/api/v1/clients \
  -H "Authorization: Bearer od_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

A key acts in one workspace and can be revoked at any time. Writes made with a key appear in the activity trail credited to the key by name.

A key carries the permissions of whoever created it, read fresh on every request, so taking someone's access away also limits their keys. A key made by someone without See costs and margin gets JSON responses with every cost, margin, fee and profit field removed, not zeroed. A key without Create and edit documents gets a 403 on the writes that need it. A key whose creator has left the workspace falls back to the viewer role. Call GET /me first from a new integration and read what it says. See Roles, permissions, two-factor and sessions.

A key acts as no one in particular, so the few operations that need a person behind them don't work with one: sending an invitation, handing on ownership, and logging time without a userId.

There is no monthly request quota. Each key has a per-minute rate: 60 a minute on Free and 300 on Unlimited. GET /billing reports what your workspace has been calling and the rate it may call at.

Health check

GET /api/health needs no key. It answers 200 with { "status": "ok", "database": "ok", "redis": "ok" } when the app can reach its database and job queue, and 503 with "status": "unavailable" and the failing check marked "down" when it can't. It sits outside /api/v1, carries no rate-limit headers and never touches your workspace. Point an uptime monitor at it.

Only a SHA-256 hash of the secret is stored. A key without See costs and margin still gets every column in a CSV download (?format=csv and the GET /export zip): only JSON responses are filtered. A key whose creator has no seat (the workspace moved to Free with more people in it than Free's one seat) gets 403 no_seat on every request until the workspace is back on the paid plan or down to one person.

Rate limits are counted in fixed one-minute windows per key.

Plans and limits

Free issues 50 invoices over the life of the workspace, with one seat. Unlimited has no invoice limit and charges per seat: $19 a month for the first and $5 for each one after, or $190 and $50 a year. Nothing else is metered on either plan. See Plans & billing for buying seats, promotion codes and refunds, none of which are API operations.

An invoice spends one of Free's allowance the first time it leaves DRAFT, and it stays spent. A draft costs nothing, and neither does deleting one. A payment you record on a draft is refused (409), but money that arrives by itself, such as an online card payment or an automatic charge, takes the draft out of DRAFT and spends one; that is never refused. Deleting, voiding or writing off an issued invoice doesn't give it back, and neither does putting it back to DRAFT and issuing it again. There's no grace period and no calendar window. Past the allowance, everything that would issue an invoice is a 402 plan_limit_reached with details.limit = "invoices": POST /invoices, the status change that issues a draft, invoicing an order or a shipment, deposit, progress, final and combined invoices, converting a quote to an invoice, and recurring runs. Opening-balance invoices, from POST /invoices/opening-balance or the invoices import, never count.

Seats. Every role uses one, the accountant included, and a pending invitation holds one from the moment it's sent. On the paid plan the limit is the seats bought, counted as members plus pending invitations; removing somebody frees a seat but doesn't lower the bill. POST /invitations past the limit is a 402 with details.limit = "seats".

Storage is unlimited, with a fair-use line of 200 GB that nothing enforces: GET /billing reports storage.overFairUse. The limits that do apply are per file and per hour, under Attachments.

Conventions

  • Base path /api/v1; JSON in and out.
  • Success responses wrap the payload: { "data": … }. Paged lists add "meta": { total, page, pageSize, pageCount } and take ?page=…&pageSize=… plus their own filters. pageSize defaults to 20 on most lists and 50 on the stock, inventory and production lists, and each list has a cap: 100 on most, 200 on the stock, inventory and production lists, and 500 on clients and items. Going over the cap is a 400. A few short lists, such as /tags, /locations, /webhooks and /members, return a plain array with no paging.
  • Sorting: the main lists (clients, items and materials, quotes, orders, invoices, credit notes, payments, vendors, purchases, inventory, spending, projects and expenses) take ?sort=field or ?sort=field:asc or field:desc (such as sort=total:desc). Each has its own set of sortable fields, and an unknown one is a 400 whose details names the valid ones. Without a direction, text sorts A→Z and dates and amounts newest or largest first. Other lists ignore sort.
  • CSV export: most lists also take ?format=csv, which streams the entire filtered result set (paging is ignored) as a CSV download in the current sort order. columns= (comma-separated ids) picks and orders the columns; leave it out for all of them. These lists have no CSV export and return JSON whatever format says: activity, client POs, imports, bins, an item's movements, production runs, time entries, quote templates, recurring invoices, stock alerts, stocktakes, the suppression list and webhook deliveries.
  • Errors are { "error": { "code", "message", "details"? } } with a matching HTTP status: 400 validation, 401 bad or missing key, 402 plan limit reached (a free workspace has issued all 50 of its invoices, or its seats are full; details.limit is invoices or seats), 403 forbidden (the key's permissions don't cover this; details.permission names the flag; no_seat when the key's creator has no seat because the workspace moved to Free with other people in it), 404 not found, 409 conflict (the write clashes with the resource's current state, such as a duplicate number, an illegal status change or a locked document), 413 payload too large, 422 unprocessable (a business rule against the data, such as shipping more than remains open), 429 rate limited, and 503 when the server isn't set up for what you asked, such as a mailbox connection with no OAuth client.
  • On most 400 validation errors details is { "formErrors": [...], "fieldErrors": { "<field>": [...] } }. The wording of a message can change, so branch on the code and on which field is named, never on the text.
  • Polling: the lists that change most (clients, items and materials, quotes, orders, invoices, credit notes, payments, vendors, purchases, projects, expenses and production runs) take ?updatedSince=<ISO timestamp> and return rows changed at or since that instant. It's inclusive: store the newest updatedAt you saw and send it back, and you see that one row again rather than missing one written in the same millisecond. Other lists ignore it and return everything.
  • Retrying a write: send an Idempotency-Key header (any unique string) on a POST, PUT, PATCH or DELETE. Retrying with the same key within 24 hours replays the first successful response, marked Idempotent-Replayed: true, instead of running the request again. A failed first attempt isn't stored, so a retry runs for real. The same key with a different method, path or body is a 409. A retry sent while the first request is still running isn't caught, so wait for an answer before retrying.
  • Rate-limit headers on every authenticated response except a 429: X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). A 429 carries details.limit and details.resetAt in its body instead.
  • Money is a decimal string everywhere, including figures the server works out rather than reads off a record: a report's revenue and margin, a dashboard money stat, a project rollup, a price suggestion, a stock valuation. Parse it as a decimal: a stored amount can drop trailing zeros ("149.5") where a computed one is padded ("149.50"). Most counts and percentages are numbers; document line quantities and tax rates, and an item's onHand and allocated, are decimal strings. Dates are ISO 8601. The one exception is a carrier network's own price: a shipping rate's amount and a bought label's amount are plain numbers, as the network quoted them.
  • Updates: documents use PATCH; most catalog and settings records use PUT. Either way only the fields you send change, except where a section says a PUT replaces the whole record.
  • Numbers: document numbers come from your configured formats when number is left out. Quotes, orders, invoices, purchases and payments accept one of your own on create, and PATCH renames a quote, order, invoice or purchase (409 when taken).
  • Statuses: derived statuses (fulfillment and payment states) are set by the app as things happen. The status endpoints set one by hand only with "override": true, which the activity trail records.
  • Lines: lines may be empty on a quote, order or invoice as long as the document still comes to more than zero, as when billing freight or tax on its own. A document that would come to nothing with no lines is rejected with a 422.
  • Line ids on an update: every line comes back with an id. When a PATCH sends lines, send those ids back with them: a line carrying its id is rewritten in place, one without is written as new, and one whose id you stop sending is deleted. That keeps billed time and expenses attached to the invoice line they were billed on, so you can reorder and edit freely as long as the ids travel with their lines. Drop them and every line is read as new, which detaches that work and hands it back to Ready to bill. An id that isn't a line on that document, or the same one sent twice, is a 422. id is ignored on POST, so a document read back and posted as a copy needs nothing stripped.
  • Discounts: documents and their lines take an optional discountType (PERCENT or AMOUNT) with a discountValue, both or neither; discountType: null on PATCH clears it. Line discounts come off quantity × unitPrice, the document discount off subtotal before tax, and responses add the resolved discountAmount at both levels.
  • Cost basis: quote, order and invoice lines return a read-only unitCost, the catalog item's unit cost when the line was written (null when unknown). It's snapshotted on create and whenever lines are replaced, and carried unchanged through convert, invoice and duplicate. A line with no itemId may set unitCost itself; on an item line it's ignored in favor of the catalog.
  • Gross margin: every order returns a read-only margin: net revenue (line totals after discounts plus shippingAmount, tax excluded) against the cost basis (each line's quantity × unitCost, every recorded shipment cost, the payment processor's fees and the expenses linked to the order, net of tax). It carries revenue, lineCost, shipmentCost, paymentFees, expenseCost, costBasis, margin and marginPercent, per line under lines, with coverage saying how many lines and shipments have a cost. A cost in another currency than the order's is left out and counted in coverage.leftOut. A line or shipment with no cost is unknown, never zero: it's left out, and margin is null when no line has one.
  • Sales documents require billTo on create (and shipTo too on an order; a quote or invoice can leave it out) and carry publicNotes (client-facing) and notes (internal), plus an optional customerPo (the client's own purchase-order reference, up to 100 characters) printed on every copy the client sees.
  • Orders also carry requestedDate (what the client asked for) and promisedDate (what you committed to). Both are optional dates, cleared by sending null, and promisedDate drives the Orders due this week dashboard widget.

Error codes

Branch on the code, never on the message.

Status code Means
400 validation_error, invalid_json, invalid_request, invalid_multipart A bad payload or query
401 missing_token, invalid_token, revoked_token The key is missing, unknown or revoked
402 plan_limit_reached Free's invoices are spent, or the seats are full; details.limit says which
403 forbidden, no_seat, read_only_key The key's permissions don't cover it (details.permission names the flag), its creator has no seat, or it is a read-only key used for anything but GET and HEAD
404 not_found No such record in your workspace
409 conflict The write clashes with the record's state: a duplicate number or SKU, an illegal status change, or a locked document (canceled, shipped, invoiced, converted or void)
413 payload_too_large An upload over the size limit
422 unprocessable, and a few named ones such as unmapped_accounts, mailbox_error, storage_unavailable and validation_failed Well-formed input that breaks a rule against your data
429 rate_limited Over the key's per-minute rate
500 internal_error A fault on our side
503 not_configured The server isn't set up for what you asked

A few 400s carry no details: an unknown format, a cost formula that doesn't parse, a missing upload.

More on lists

pageSize defaults to 25 on projects, expenses and client POs, and a price list's items default to 100 and cap at 500. /custom-fields, /item-categories and /invitations are plain arrays too. On the document lists, dateFrom and dateTo are inclusive yyyy-mm-dd dates on issueDate (quotes, orders and invoices) or orderDate (purchases).

To poll with updatedSince, sort the list sort=updated:asc and page through.

In a CSV, an unknown id in columns= is a 422 naming the valid ones. The column ids match the list's columns in the app, for orders number,status,client,company,source,customerPo,tags,issued,requested,promised,subtotal,tax,shipping,total,margin,marginAmount,marginCoverage,updated. The clients CSV always carries the address book (email, phone, website, paymentTerms, notes, the six billing* and six shipping* columns and otherContacts), whatever columns= asks for, so it loads straight back through POST /imports.

More on writes

Only a JSON response replays under an Idempotency-Key: a PDF replays as {}, and a 204 can't be replayed (it fails with a 500). A 401 carries no rate-limit headers.

Unit costs are the one money figure held to four decimals rather than two: averageCost, a production run's costs, a landed cost and a recipe's unitCost come back like "6.1000".

Default document numbers look like Q-000001, SO-000001, INV-000001, PO-000001 and PMT-000001. Setting the number on create is how to avoid a gap: a number the counter already handed out isn't given back when a document is renamed.

Discounts carry through convert, invoice, invoicing per shipment (a fixed amount is prorated to the shipped share) and duplicate. So does each line's unitCost.

An order's margin carries marginPercent to two decimals, a lines array of { revenue, cost, margin, marginPercent }, and coverage: { costedLines, totalLines, costedShipments, totalShipments, leftOut, freightPending, complete }. freightPending is true while shipping is charged and no shipment has a known freight cost, and leftOut lists { kind, currency, count } for costs in another currency that could not be added. When only some lines have a cost, the margin compares those lines' revenue (with their share of the document discount) against their cost.

A document's terms text is a snapshot: renaming a payment term later doesn't rewrite an invoice already sent. billTo and shipTo are free-text snapshots too, stable when the client's saved addresses change; they can be changed on PATCH but not cleared (an empty value keeps the stored one). customerPo is cleared with null, copied across when a quote converts or an order is invoiced, and ?customerPo= matches part of it, ignoring case.

An order's tags are up to 50 strings of up to 40 characters, and a PATCH that sends tags replaces them.

Endpoints

Every operation, grouped as the explorer groups them. Paths are relative to /api/v1 unless they say otherwise; the explorer has each one's parameters, request body and response.

Organization

Who the key acts as, billing, dashboard data, search.

Method Path What it does
GET /organization The organization this key acts as, with its profile
GET /organization/profile The letterhead, currency, locale and logo
PUT /organization/profile Replace the profile
GET /organization/logo The logo file
PUT /organization/logo Upload (replace) the logo
DELETE /organization/logo Remove the logo
GET /billing Plan, subscription and usage
GET /billing/account-credit Account credit and the ledger behind it
GET /dashboard Headline numbers
GET /dashboard/widgets Dashboard widget data for a date range
GET /search Global search across the workspace

Health

Whether the deployment is up, and this API's own description. No key needed.

Method Path What it does
GET /api/health Is the deployment up
GET /openapi.json This API description

Custom fields & bulk

Fields a workspace adds to its own records, and doing one thing to many records at once.

Method Path What it does
GET /custom-fields The fields this workspace added to its own records
POST /custom-fields Define a field
POST /custom-fields/reorder Set the order one entity's fields appear on the form
GET /custom-fields/{id} One definition
PUT /custom-fields/{id} Change the label, type, choices or where it shows
DELETE /custom-fields/{id} Remove a definition
GET /bulk-jobs/{id} How a long bulk run is getting on
POST /orders/bulk Do one thing to many orders
POST /quotes/bulk Do one thing to many quotes
POST /invoices/bulk Do one thing to many invoices
POST /items/bulk Do one thing to many items
POST /clients/bulk Do one thing to many clients
POST /purchases/bulk Do one thing to many purchases

invoice on orders closes each one out as its own draft invoice, deducting what has already been paid on it. With payload.combine: true, each client's selected orders go on one combined draft instead. It makes drafts, not emails. A draft order, or one already invoiced, fails on its own row.

Clients, items, orders, quotes, invoices, vendors, purchases, projects and expenses accept and return customFields (a merge: sending one field never clears the others) and filter on cf.<key>: ?cf.account_no=4471, ?cf.notes=contains:gate, ?cf.visit=after:2026-01-01. A text field without a prefix matches anywhere in the value.

An explicit null or "" clears one custom field. Keys nobody has defined are dropped rather than refused, so a save never fails because a field was deleted last week. Values are checked against the definition: a DATE field refuses "the fourth", and a SELECT takes only its own choices (in any case, stored as the workspace spelled them). entity is CLIENT, ITEM, ORDER, QUOTE, INVOICE, VENDOR, PURCHASE, PROJECT or EXPENSE.

?cf.scent_family=Citrus,Woody      any of
?cf.visit=after:2026-01-01         or before:
?cf.reorder=true

A bulk action's payload carries what the action needs: { status }, { tag }, { promisedDate }, { source }, { categoryId }, { taxGroupId }, { trackStock }, { paymentTermId }, { combine } or { price }. The actions that need a mailer, a PDF or a download (email, send, send-reminder, packing-slips, export) work in the app only: through the API each id fails with a sentence saying so.

Catalog tree

The managed category tree, the shared tag list, and archiving a document instead of deleting it.

Method Path What it does
GET /item-categories The catalog tree
POST /item-categories Add a category
POST /item-categories/reorder Reorder one level of the tree
GET /item-categories/{id} One category with its children
PUT /item-categories/{id} Rename, re-file, or change what its items start from
DELETE /item-categories/{id} Delete an empty category
POST /item-categories/{id}/merge Fold one category into another
GET /tags The workspace's one tag list
POST /tags Add a tag
GET /tags/{id} One tag with its usage count
PUT /tags/{id} Rename or recolour a tag
DELETE /tags/{id} Take a tag off everything and drop it
POST /tags/{id}/merge Fold one tag into another
POST /quotes/{id}/archive Archive a quote
POST /quotes/{id}/restore Restore a quote from the archive
POST /orders/{id}/archive Archive a order
POST /orders/{id}/restore Restore a order from the archive
POST /invoices/{id}/archive Archive a invoice
POST /invoices/{id}/restore Restore a invoice from the archive
POST /credit-notes/{id}/archive Archive a credit note
POST /credit-notes/{id}/restore Restore a credit note from the archive
POST /purchases/{id}/archive Archive a purchase
POST /purchases/{id}/restore Restore a purchase from the archive

Those five list endpoints take archived=true|false|all. Deleting a quote, order, invoice or purchase past DRAFT returns 409 and names archive instead. DELETE /credit-notes/{id} voids the note at any status, and is a 409 once any of it has been applied or refunded. Items take categoryId and filter by it (children included) or uncategorised=true.

Setup

Finding your feet: the setup checklist, and the opening balances a workspace carries in.

Method Path What it does
GET /setup/checklist The guided-setup checklist
POST /setup/checklist Tick a step off by hand, or dismiss the card
POST /inventory/opening-balances What was on the shelf when the workspace opened
POST /invoices/opening-balance An unpaid invoice that predates the workspace

Webhooks

Being told when something happens instead of asking: signed deliveries, retries and the delivery log.

Method Path What it does
GET /webhooks The workspace's endpoints
POST /webhooks Add an endpoint
GET /webhooks/{id} One endpoint
PUT /webhooks/{id} Change an endpoint
DELETE /webhooks/{id} Remove an endpoint and its delivery history
POST /webhooks/{id}/test Queue an obviously fake event so you can watch one arrive
POST /webhooks/{id}/rotate-secret Mint a new signing secret and return it once
GET /webhooks/{id}/deliveries What was sent, what came back, and when the next attempt is due
POST /webhook-deliveries/{id}/redeliver Send a settled delivery again
POST /webhooks/subscriptions REST-hook subscribe (Zapier)
DELETE /webhooks/subscriptions REST-hook unsubscribe (Zapier)

Deliveries are signed X-OrderDen-Signature: t=<unix>,v1=<hmac-sha256(secret, t + "." + body)> and retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. An endpoint switches itself off after 25 deliveries in a row that used up every attempt. Full details, including the verification code, are in Webhooks.

Team & access

Who is in the workspace, the role and permission flags each of them carries, and the invitations still open.

Method Path What it does
GET /me What this key is and what it may do
GET /members Everybody in the workspace
POST /members/{id}/transfer-ownership Transfer ownership
GET /members/{id} One member
PUT /members/{id} Change somebody's role and permissions
DELETE /members/{id} Remove somebody from the workspace
GET /invitations Invitations still open
POST /invitations Invite somebody
DELETE /invitations/{id} Withdraw an invitation

Nobody grants access they don't hold themselves, and the owner's row isn't editable here. Every role uses a seat. A role change never returns a 402, because it never changes what the workspace pays. There is no POST /members: people join by accepting an invitation.

Roles are owner, admin, member, accountant and viewer. The eight permission flags are seeCosts, editDocuments, recordPayments, manageClients, manageCatalog, manageStock, viewReports and exportData.

Activity

The org-wide activity trail.

Method Path What it does
GET /activity The activity trail, newest first

Reports

Revenue, margin and fulfillment reports over a date range.

Method Path What it does
GET /reports/revenue-by-client Revenue and gross margin by client
GET /reports/wholesale-vs-retail Trade against retail, compared on margin
GET /reports/revenue-by-item Revenue and gross margin by catalog item, or by category
GET /reports/revenue-by-period Revenue and gross margin per day, week or month
GET /reports/recurring-revenue What the running plans are worth a month
GET /reports/failed-charges Invoices whose card on file was declined
GET /reports/quote-conversion What became of the period's quotes, and which options were taken
GET /reports/backlog-aging Open order backlog aged by promised date
GET /reports/fulfilment-cycle-time Days from confirmation to fully shipped
GET /reports/on-time-delivery Share of orders fully shipped by their promised date
GET /reports/inventory-valuation What the stock was worth, as at any date
GET /reports/stock-movements The stock ledger over a period
GET /reports/stock-by-location What stock is where, right now
GET /reports/stocktake-variance What the physical counts found
GET /reports/production-summary What was made in a period and what it cost
GET /reports/project-profitability What each project was quoted at and what it cost
GET /reports/expenses-by-category Where the money went, per spend category
GET /reports/profit-and-loss Revenue, cost of sales, expenses and what is left
GET /reports/materials-usage What production got through, waste separately
GET /reports/credits-and-refunds What was given back, and what for
GET /reports/ar-ageing Who owes what, and for how long
GET /reports/cash-flow-forecast What's expected in over the next eight weeks
GET /reports/income-by-payment-method How the money arrived
GET /reports/tax-collected Tax charged per rate and period
GET /reports/cogs Cost of goods sold for a period
GET /reports/schedule-c-expenses The period's spend grouped by tax line

Every report takes range (the dashboard presets; default this_month) with from/to for range=custom, and ?format=csv (with the list exports' columns= picker) streams the report's rows. The two tax-time reports are the exception: they take a plain from/to pair of calendar dates, and their CSVs ignore columns=. See the Reports guide and Tax time for what each one measures.

range is one of today, this_week, last_7_days, last_30_days, this_month (the default), last_month, this_quarter, last_quarter, this_year, last_year, all_time or custom. In a CSV, an unknown id in columns= is a 422 naming the valid ones. An unknown report is a 404 whose details.validReports lists them all.

Clients

Clients with contacts, contact methods and addresses.

Method Path What it does
GET /clients List clients
POST /clients Create a client
GET /clients/duplicates Candidate duplicate clients, grouped
POST /clients/merge Fold duplicate clients into one
GET /clients/{id} Get a client (full graph incl. activity)
PATCH /clients/{id} Update a client
DELETE /clients/{id} Delete or archive a client
POST /clients/{id}/restore Restore an archived client
GET /clients/{id}/summary Cross-domain rollup (documents, money, items)
POST /clients/{id}/notes Add a note to the client's activity feed
PATCH /client-notes/{id} Edit a note
DELETE /client-notes/{id} Delete a note
POST /clients/{id}/addresses Add an address
POST /clients/{id}/contacts Add a contact person
POST /clients/{id}/contact-methods Add a contact method (email/phone/…)
PUT /addresses/{id} Replace an address
DELETE /addresses/{id} Remove an address
PUT /contacts/{id} Replace a contact
DELETE /contacts/{id} Remove a contact
PATCH /contact-methods/{id} Edit a contact method
DELETE /contact-methods/{id} Remove a contact method
GET /clients/{id}/attachments List the client's attachments
POST /clients/{id}/attachments Upload an attachment to the client
GET /clients/{id}/credit A client's credit balance and the ledger behind it

A merge repoints every row that references a merged client (every document, payment, address, contact, portal account, project, file and activity entry) onto keepId in one transaction, applies fieldChoices to the survivor, adds up credit balances, and archives the emptied records rather than deleting them. Tags are combined unless fieldChoices sends tags, which replaces them. Ten at a time at most, and a 409 rather than a half-merge when two of them hold a portal sign-in for the same address. See Clients.

Items

The sellable catalog: pricing and COGS decision support.

Method Path What it does
GET /items List catalog items
POST /items Create an item
GET /items/{id} Get an item
PATCH /items/{id} Update an item
DELETE /items/{id} Delete an item
GET /items/{id}/vendors Who sells this item
PUT /items/{id}/vendors Add or update a vendor on the item
DELETE /items/{id}/vendors/{vendorId} Take a vendor off the item
POST /items/{id}/archive Archive an item
POST /items/{id}/restore Restore an item
POST /items/{id}/duplicate Duplicate an item

Stock

Quantity on hand from the movement ledger, adjustments, opening balances and low-stock alerts.

Method Path What it does
GET /inventory What is on the shelf
GET /inventory/bins Every bin the workspace has recorded
PUT /inventory/bins Set where one thing sits
GET /inventory/movements The workspace's whole stock ledger
GET /items/{id}/stock An item's stock figures
GET /items/{id}/movements An item's stock ledger
POST /items/{id}/movements/{movementId}/reverse Reverse an adjustment, opening balance or count
POST /items/{id}/revalue Revalue a stocked item
POST /items/{id}/stock/adjust Adjust an item's stock
POST /items/{id}/stock/opening Set an item's opening stock
GET /stock/alerts Low-stock alerts
GET /stock/allocations Allocations against an item
POST /stock/allocations Allocate stock to an order line
GET /stock/allocations/{id} Get an allocation
DELETE /stock/allocations/{id} Release an allocation
GET /orders/{id}/allocations Allocations on an order
GET /items/{id}/stock-by-location Where an item's stock is
GET /items/{id}/bins Where an item sits
PUT /items/{id}/bins Set where an item sits

An item sold in variants needs variantId on an adjustment or an opening balance (409 without one). See Inventory & stock for what the figures mean.

Inventory and bins

Items and inventory are the same rows answering different questions. /items is the catalog; these are the shelf.

A bin is free text, such as "A3" or "top shelf, left", and a thing can be in several at one place. Location is the coarse where (studio, lock-up, van); the bin is the fine one. No bin carries a quantity: the ledger counts stock per location, so bins say where to look, not how many are in each.

See Bins.

Confirming an order changes no stock figure: allocated moves only when somebody sets stock aside with POST /stock/allocations. An item sold in variants reports onHand summed over its variants, at an average cost weighted by what each holds.

Variants & kits

Product variants with their own SKU, price and stock; kits made of other items; item photographs.

Method Path What it does
GET /items/{id}/variants An item's variants
POST /items/{id}/variants Save the variant grid
POST /items/{id}/variants/generate Propose the variant grid
GET /variants/{id} Get a variant
PUT /variants/{id} Update a variant
DELETE /variants/{id} Delete a variant
GET /items/{id}/components What a kit contains
PUT /items/{id}/components Set what a kit contains
GET /items/{id}/images An item's photographs
POST /items/{id}/images Upload a photograph
PUT /items/{id}/images Reorder an item's photographs
DELETE /items/{id}/images/{attachmentId} Remove a photograph

Document lines accept variantId and personalization; a line naming an item with variants must carry one (422 otherwise). Stock lives on the variant, so the adjust and opening-stock calls take a variantId too. See Items & pricing.

Production

Making stock rather than buying it: runs that consume a recipe's materials and produce a costed batch, and how many more you can make from what is in stock.

Method Path What it does
GET /production-runs List production runs
POST /production-runs Plan a production run
GET /production-runs/{id} Get a production run
PUT /production-runs/{id} Change a planned or in-progress run
DELETE /production-runs/{id} Delete a run that never moved stock
POST /production-runs/{id}/void Void a completed run
POST /production-runs/{id}/reopen Reopen a completed run to correct it
POST /production-runs/{id}/start Start a planned run
POST /production-runs/{id}/complete Finish the batch and move the stock
POST /production-runs/{id}/cancel Call off a run that has not completed
GET /items/{id}/buildable How many more can be made from what is on hand

Making stock rather than buying it. A run scales the item's recipe to the quantity being made; nothing moves on the stock ledger until it completes, so a planned run is free to cancel. Completing uses up the materials, books any waste as an adjustment, puts the finished batch on the shelf at what it cost, and freezes that cost so a later price change never rewrites it. See Production.

Items carry madeToOrder: confirming an order opens a planned run for each line whose item is made to order, tracks stock, isn't a kit and has an active recipe. What a completed run made is sellable straight away.

A run opened for an order line is linked by orderLineId, and a line that already has a live run gets no second one. Canceling the order cancels its planned runs.

Materials & recipes

What a workspace buys in and consumes, the units it counts in, the numbers every product shares, and the bill of materials that turns all three into a cost per unit.

Method Path What it does
POST /materials/{id}/revalue Revalue a material
GET /items/{id}/recipe An item's recipe (bill of materials)
PUT /items/{id}/recipe Save an item's recipe
DELETE /items/{id}/recipe Remove an item's recipe
GET /items/{id}/recipe/cost What the bill of materials works out to
POST /items/{id}/recipe/adopt Use the bill of materials cost as the item's unit cost
GET /materials List materials
POST /materials Create a material
GET /materials/{id} Get a material
PUT /materials/{id} Update a material
DELETE /materials/{id} Delete a material
GET /units List units of measure
POST /units Add a unit
GET /units/{id} Get a unit
PUT /units/{id} Update a unit
DELETE /units/{id} Delete a unit
GET /variables List workspace variables
POST /variables Add a workspace variable
GET /variables/{id} Get a workspace variable
PUT /variables/{id} Update a workspace variable
DELETE /variables/{id} Delete a workspace variable

The costing side. Materials are catalog items of type MATERIAL, so every /items endpoint works on them too; these routes scope the list and force the type. See Materials, recipes and what things cost.

Items carry unitId, costSource (MANUAL, RECIPE or PURCHASES) and the read-only recipeUnitCost.

Client POs

Client POs portal clients handed over, and turning them into draft orders.

Method Path What it does
GET /client-pos List client POs
GET /client-pos/{id} Get one
GET /client-pos/{id}/files The document they sent
POST /client-pos/{id}/accept Accept it as a draft order
POST /client-pos/{id}/decline Decline it, with a reason
POST /client-pos/{id}/reopen Put it back to waiting
POST /client-pos/{id}/read Read the document again

Submitting one is the portal's own, not /api/v1: only a signed-in portal client can send one. The workspace switch is portalPoEnabled on /settings/templates; the per-client exception is set in the app. See The client portal.

portalPoEnabled is off for the workspace by default and on for each client by default, so turning the workspace switch on opens it to every client but the ones you've switched off.

Quotes

Method Path What it does
GET /quotes List quotes
POST /quotes Create a quote
GET /quotes/{id}/chain Everything tied to this quote's job
GET /quotes/{id} Get a quote
PATCH /quotes/{id} Update a quote
DELETE /quotes/{id} Delete a quote
POST /quotes/{id}/status Set a quote's status
POST /quotes/{id}/convert Convert a quote into an order
POST /quotes/{id}/apply-to-order Apply a converted quote's changes to its order
POST /quotes/{id}/convert-to-invoice Invoice a quote directly
POST /quotes/{id}/convert-to-project Start a project from a quote
GET /quotes/{id}/select-options How a quote's options stand
POST /quotes/{id}/select-options Set which options a quote charges for
POST /quotes/{id}/save-as-template Save a quote as a reusable template
GET /quotes/{id}/share-link Get the quote's active share link
POST /quotes/{id}/share-link Create (or rotate) the quote's share link
DELETE /quotes/{id}/share-link Revoke the quote's share link
GET /quotes/{id}/attachments List the quote's attachments
POST /quotes/{id}/attachments Upload an attachment to the quote
GET /quote-templates List quote templates
POST /quote-templates Create a quote template
GET /quote-templates/{id} Get a quote template
PUT /quote-templates/{id} Update a quote template
DELETE /quote-templates/{id} Delete a quote template
POST /quote-templates/{id}/create-quote Raise a quote from a template
POST /quotes/{id}/duplicate Duplicate a quote
GET /quotes/{id}/pdf The quote as a PDF
GET /quotes/{id}/signature The client's signature on a quote
POST /quotes/{id}/signature Record a signature taken in person
POST /quotes/{id}/signature/void Ask for a new signature

Quote lines also accept isOptional, isSelected, optionGroup and optionGroupLabel. The quote's total counts the selected lines alone, and it exposes optionalTotal (what's on offer and not taken, before tax) and selectionLockedAt. A group of one item at several quantities is shown as a price-break table. See Quotes.

Quote templates

Signatures on quotes

Quotes accept requireSignature and termsText (Markdown, snapshotted onto the quote) and expose the signature they carry.

GET /quotes/{id}/signature reads it; POST /quotes/{id}/signature records one taken in person: signerName, signerEmail, method (TYPED or DRAWN), a base64 PNG image for a drawn one, and consented: true. The hash is never taken from the request: the server fingerprints the quote as it stands, so a signature always points at what the workspace has. One document, one signature: a second is a 409.

The fingerprint covers the quote's number, currency, client, issue date, lines, totals, deposit and terms, not the PDF bytes, so rendering it again never invalidates the signature. Editing any of those does; editing the addresses, notes, customer PO or valid-until date doesn't.

Changing what was agreed means editing the order the quote became: PATCH /orders/{id} moves its lines, its money and the tax on them together, and the order's history records what changed.

See Signatures on quotes.

Orders

Orders, shipments and fulfillment.

Method Path What it does
GET /orders List orders
POST /orders Create an order
GET /orders/{id}/chain Everything tied to this order's job
GET /orders/{id} Get an order (lines, shipments, invoices)
PATCH /orders/{id} Update an order
DELETE /orders/{id} Delete an order
POST /orders/{id}/status Set an order's status
POST /orders/{id}/reopen Reopen a completed order
POST /orders/{id}/duplicate Duplicate an order (reorder)
POST /orders/{id}/lines/{lineId}/reopen Reopen quantity closed short
POST /orders/{id}/close-short Close an order short
POST /orders/{id}/shipments Record a shipment (partial fulfillment)
PATCH /orders/{id}/shipments/{shipmentId} Edit a shipment
DELETE /orders/{id}/shipments/{shipmentId} Void a shipment
PATCH /shipments/{id} Edit a shipment
POST /orders/{id}/shipments/{shipmentId}/invoice Invoice a single shipment
POST /orders/{id}/invoice Invoice what the order has shipped, or what is left of it
GET /orders/{id}/share-link Get the order's active share link
POST /orders/{id}/share-link Create (or rotate) the order's share link
DELETE /orders/{id}/share-link Revoke the order's share link
GET /orders/{id}/attachments List the order's attachments
POST /orders/{id}/attachments Upload an attachment to the order
GET /orders/{id}/pdf The order as a PDF
GET /orders/{id}/packing-slip.pdf The packing slip
POST /orders/packing-slips A stack of packing slips as one PDF
GET /orders/{id}/pick-list The sheet you walk the shelf with

Carriers

The workspace's shipping-carrier list.

Method Path What it does
GET /carriers List carriers
POST /carriers Add a carrier
GET /carriers/{id} Get a carrier
PATCH /carriers/{id} Update a carrier
DELETE /carriers/{id} Delete a carrier

The workspace's shipping-carrier list (Company Settings → Shipping). Shipment.carrier references a carrier by name; a carrier's trackingUrlTemplate (containing a {tracking} token) is what turns tracking numbers into links.

Every workspace starts with UPS, FedEx, USPS and DHL. The tracking links appear on the order page, the printed and emailed documents, and the client portal.

Shipping

Buying postage from the order through the workspace's own carrier account: rates from every carrier side by side, labels, batches, tracking and the boxes it all goes in.

Method Path What it does
GET /settings/shipping The carrier networks
PUT /settings/shipping Save a connected network's settings
DELETE /settings/shipping Disconnect a network
GET /package-presets List the boxes
POST /package-presets Add a box
GET /package-presets/{id} Get a box
PUT /package-presets/{id} Update a box
DELETE /package-presets/{id} Delete a box
POST /addresses/{id}/validate Check an address against the carrier network
POST /orders/{id}/shipping/rates What every carrier would charge
POST /orders/{id}/shipping/labels Buy a label
POST /orders/{id}/shipping/local Record a collection or a local delivery
POST /shipping/labels/batch Buy labels for a stack of orders
GET /shipments/{id}/label.pdf Reprint a bought label
POST /shipments/{id}/void Ask for the postage back

Buying postage through the workspace's own carrier account: Easyship or Shippo. Rating an order writes nothing, so ask as often as you like. Buying a label records the shipment with what the postage cost, which keeps the order's margin right, and keeps the PDF.

Millimeters and grams throughout, because that's what the columns hold; money is in the account's own currency.

Two networks are carried, Shippo for the US carriers and Easyship for parcels going abroad, and a workspace connects either or both. The account is the workspace's and so is the postage: we're never in the middle of the money. A refund or a tracking lookup always goes back to the network that sold the label, whatever is connected since. Every other call that talks to a carrier uses the earliest-connected account when it isn't told which.

Tracking. Every bought label is tracked. When the carrier reports delivery, the shipment moves to DELIVERED with the carrier's own scan time, the buyer gets the Order delivered email if that's switched on, and the workspace owner is notified. Only delivery changes anything, and a delivery reported twice sends one email.

A stack of documents rendered as one PDF on the background worker, polled for rather than waited on.

Method Path What it does
POST /print-batches Book a stack of documents to be printed
GET /print-batches/{id} How a print batch is getting on
GET /print-batches/{id}/file The finished PDF

A stack of documents rendered as one PDF, without holding a request open while it happens. Laying out forty packing slips or merging forty carrier labels is seconds of work, so it runs on the worker and you poll for the file.

An id that can't be rendered, such as a shipment with no label bought or an order with nothing to pack, comes back in missing rather than failing the batch. When none of them render, the batch is FAILED.

Pricing

What to charge for something, and what a price actually leaves you once the goods and the postage are paid for.

Method Path What it does
GET /items/{id}/pricing What to charge, and what a price actually leaves you
POST /items/{id}/pricing Put a suggested price on a price list
POST /pricing/what-if What if I charged this instead?
GET /settings/pricing The workspace's margin target and markup rules
PUT /settings/pricing Update the pricing rules

What to charge, and what a price leaves you. Read-only except for the one write that sets a price, which lands in the item's history saying where the number came from.

The target is a margin, what's left after the goods and the postage are paid for, not a markup. Postage you charge is revenue and postage you pay is a cost, so absorbing it moves the answer.

Doubling a cost is a 100% markup and a 50% margin, and once the postage is out of it, it's neither.

Stocktakes

The physical count: what is actually on the shelf, as opposed to what the ledger thinks.

Method Path What it does
GET /stocktakes List stocktakes
POST /stocktakes Start a count
GET /stocktakes/{id} Get a stocktake
PUT /stocktakes/{id}/lines/{lineId} Record what was found
DELETE /stocktakes/{id}/lines/{lineId} Take a line off a count
POST /stocktakes/{id}/lines Add items to a count under way
POST /stocktakes/{id}/reverse Reverse a posting
POST /stocktakes/{id}/resume Pick a canceled count back up
GET /stocktakes/{id}/review What the count found
POST /stocktakes/{id}/post Post the count
POST /stocktakes/{id}/cancel Abandon a count

See Counting stock.

Stocktakes are numbered like ST-000001.

Barcodes

Turning a scanned code into a thing, and printing the stickers that carry it.

Method Path What it does
GET /barcodes/{code} What a scanned code is
POST /items/barcodes/generate Give a code to whatever has none
POST /labels Print barcode labels

Items and variants accept barcode and barcodeType (EAN13, UPCA, CODE128, QR); leave the type out and it's inferred. A code belongs to exactly one thing, and a duplicate is a 409 naming what already has it.

See Barcodes and labels.

012345678905 is a valid UPC-A and an invalid EAN-13, which is why barcodeType can be sent rather than inferred.

Locations

Where the workspace's stock is: the studio, storage, the van.

Method Path What it does
GET /locations List locations
POST /locations Add a location
GET /locations/{id} Get a location
PUT /locations/{id} Update a location
DELETE /locations/{id} Delete a location

Orders accept locationId on create: the shelf the order is picked from. See Inventory & stock.

Every workspace starts with one location, the default. A location is a name: nothing in OrderDen depends on what kind of place it is. Moving stock between two of your own locations is two adjustments: one off the place it left, one onto the place it reached.

Price lists

Named sheets of prices (retail, wholesale and any others) with the minimum and case pack each line is sold on, and what a given buyer actually pays.

Method Path What it does
GET /price-lists List price lists
POST /price-lists Add a price list
GET /price-lists/{id} Get a price list
PUT /price-lists/{id} Update a price list
DELETE /price-lists/{id} Delete a price list
GET /price-lists/{id}/items The list's prices
PUT /price-lists/{id}/items Set or clear prices in bulk
POST /price-lists/{id}/apply-rule Fill the list from its markup rule
POST /price-lists/{id}/undo-fill Undo the last fill from the rule
GET /items/{id}/prices This item on every price list
POST /pricing/resolve What does this buyer pay?

Named sheets of prices, with the minimum and case pack each line is sold on. See Wholesale and retail pricing.

Clients accept priceListId, and a document line accepts one too, for a one-off trade price on an otherwise retail document. A line left at zero takes what the lists say; a price you send is kept as sent.

A row on a list can carry quantity breaks (bands): "from 100, $0.95". A line takes the band its quantity falls in, on whichever list the resolution order picked.

A price you send on a line is kept exactly as sent and marked MANUAL. The terms are enforced on documents too: a quantity below a sheet's minimumQuantity, or not a whole multiple of its casePack, is a 422 naming the number that would work, whether the price came from the sheet or was typed. A price-list import needs the list to exist first, so a typo can't quietly create a new one.

Taxes

The workspace's tax rates and the groups that combine them.

Method Path What it does
GET /tax-rates List tax rates
POST /tax-rates Add a tax rate
GET /tax-rates/{id} Get a tax rate
PUT /tax-rates/{id} Update a tax rate
PATCH /tax-rates/{id} Update a tax rate (same as PUT)
DELETE /tax-rates/{id} Delete a tax rate
GET /tax-groups List tax groups
POST /tax-groups Combine rates into a group
GET /tax-groups/{id} Get a tax group
PUT /tax-groups/{id} Update a tax group
PATCH /tax-groups/{id} Update a tax group (same as PUT)
DELETE /tax-groups/{id} Delete a tax group

The workspace's tax settings (Company Settings → Taxes). A rate is one named percentage; a group charges one or more of them together, in position order, and is what a document line points at. Creating a rate also creates the single-rate group that makes it pickable, so /tax-groups is the list to read when populating a picker.

On documents. Every line payload takes taxGroupId (omit it to fall back to the item's default and then the workspace's; send null for no tax) and taxable (false leaves the line out of tax entirely). Documents take shippingTaxGroupId for tax on the shipping charge; null, the default, leaves shipping untaxed. Responses carry taxAmount plus a taxBreakdown: [{ "name", "rate", "amount" }], one entry per rate the document charged. Each line carries the taxRate (the group's effective combined percentage) and taxAmount snapshotted when it was written. A client with taxExempt: true is never charged, whatever a line asks for.

A client with taxExempt: true gets its lines back as taxable: false with no group, whatever the request asked for, and its taxExemptReason and taxId print on its documents.

A document discount is shared across every line in proportion to its total (to the cent, leftover cents on the largest line), and each taxable line is taxed on what's left of it. Sharing it over the untaxed lines too keeps a mixed document from handing its whole discount to the taxable lines and undercharging tax.

Invoices

Invoices and their payments.

Method Path What it does
GET /recurring-invoices List recurring invoices
POST /recurring-invoices Set up a recurring invoice
GET /recurring-invoices/{id} Get a recurring invoice
PUT /recurring-invoices/{id} Change the plan
DELETE /recurring-invoices/{id} Delete a recurring invoice
POST /recurring-invoices/{id}/pause Pause a plan
POST /recurring-invoices/{id}/resume Resume a paused or canceled plan
POST /recurring-invoices/{id}/run Raise the next invoice now
POST /invoices/{id}/duplicate Duplicate an invoice
GET /invoices List invoices
POST /invoices Create an invoice
POST /invoices/{id}/rebuild Rebuild an invoice from its order
GET /invoices/{id}/chain Everything tied to this invoice's job
GET /invoices/{id} Get an invoice (lines and payments)
PATCH /invoices/{id} Update an invoice
DELETE /invoices/{id} Delete an invoice
POST /invoices/{id}/status Set an invoice's status
POST /invoices/{id}/payments Record a payment
GET /invoices/{id}/pdf The invoice as a PDF
POST /invoices/{id}/make-recurring Bill this invoice again and again
DELETE /invoices/{id}/late-fees/{lineId} Take a late fee off an invoice
POST /invoices/{id}/write-off Write the balance off as a bad debt
DELETE /invoices/{id}/write-off Reverse a write-off
DELETE /invoices/{id}/payments/{paymentId} Remove a payment
POST /invoices/{id}/payments/{paymentId}/refund Refund a payment
GET /invoices/{id}/share-link Get the invoice's active share link
POST /invoices/{id}/share-link Create (or rotate) the invoice's share link
DELETE /invoices/{id}/share-link Revoke the invoice's share link
GET /invoices/{id}/attachments List the invoice's attachments
POST /invoices/{id}/attachments Upload an attachment to the invoice

Payments on an invoice carry number (the receipt number), method, methodNote, source (MANUAL · ONLINE · IMPORTED), feeAmount and netAmount (the processor's cut and what reached the workspace; null on manual payments), refundedAmount and their refunds. An invoice settled in full also carries paidAt, the date of the payment that cleared it (null on anything unpaid or reopened by a refund); it's sortable as sort=paidDate and exports as the paidDate column.

Recurring invoices, cards on file and late fees

Saving a payment method isn't an API operation: it needs the processor's own form in front of a person. See Recurring invoices.

Processor fees never change what the client owes.

Payments

Every payment in the workspace.

Method Path What it does
GET /clients/{id}/payment-methods A client's cards and bank accounts on file
PUT /payment-methods/{id} Make a payment method the client's default
DELETE /payment-methods/{id} Take a payment method off file
POST /invoices/{id}/charge Charge a card on file
GET /payments/{id}/receipt.pdf A receipt for one payment
POST /payments/{id}/receipt/email Email a receipt for one payment
GET /payments List payments
POST /payments Record a payment against any invoice
GET /payments/summary This month's payments, summed
GET /payments/{id} Get a payment
PATCH /payments/{id} Correct a payment's details
DELETE /payments/{id} Delete a payment
DELETE /refunds/{id} Undo a refund
PUT /payments/{id}/allocations Apply a payment to invoices

The same records read the other way round: every payment in the workspace, not one invoice's.

Rows carry the invoice they settle, when they settle one, with its number, currency and total, plus that invoice's client. A payment against no invoice is client credit. Deleting and refunding stay on the invoice's own sub-resource above.

Payment receipts

A payment refunded in part reduces every invoice it settled in proportion to what each took.

Credit notes

Correcting a sale without pretending it never happened: what came back, where the money went, and the bad debt that is neither.

Method Path What it does
GET /credit-notes List credit notes
POST /credit-notes Draft a credit note
GET /credit-notes/{id} Get a credit note (lines, applications and refunds)
PUT /credit-notes/{id} Edit a credit note
DELETE /credit-notes/{id} Delete a draft, or void an issued credit note
POST /credit-notes/{id}/issue Issue the credit note
POST /credit-notes/{id}/apply Apply credit to an invoice
POST /credit-notes/{id}/unapply Take credit back off an invoice
POST /credit-notes/{id}/refund Refund the credit
POST /credit-notes/{id}/void Void a credit note
GET /credit-notes/{id}/pdf The credit note as a PDF
POST /credit-notes/{id}/email Email the credit note to the client

Correcting a sale without pretending it never happened. See Credit notes.

POST /payments takes allocations[] ([{ invoiceId, amount }]), so one payment can settle several invoices, plus autoAllocate to spread the rest over the client's open invoices, oldest first. Whatever is left over becomes their credit. Clients carry creditBalance.

Attachments

Files by id (upload/list live under each record)

Method Path What it does
GET /attachments/{id} Attachment metadata
PUT /attachments/{id} Show a file to the client, or stop showing it
PATCH /attachments/{id} Rename or move an attachment
DELETE /attachments/{id} Delete an attachment (permanent)
GET /attachments/{id}/download Download the file bytes
GET /attachment-purges Past attachment clear-outs
POST /attachment-purges Export and remove attachments older than a date
POST /attachment-purges/preview What a cutoff would remove
GET /attachment-purges/{id} One clear-out
DELETE /attachment-purges/{id} Delete the archive from OrderDen storage
GET /attachment-purges/{id}/download The archive a clear-out produced

GET/POST/DELETE /{doc}/{id}/share-link on quotes, orders and invoices: create (or rotate with { "rotate": true }), read and revoke the document's portal link. url is the full address to hand the client.

GET/POST /{parent}/{id}/attachments on clients, quotes, orders, invoices, purchases and vendors (base64 data, up to 10 MB), plus GET /attachments/{id}, GET /attachments/{id}/download and DELETE /attachments/{id}.

Two things happen to an upload on the way in. An image is downscaled to 2,000 px on its long edge and re-encoded, keeping its filename and content type. PDFs and anything that isn't a photo pass through untouched, so the bytes you get back from /download may be fewer than the bytes you sent. And a workspace has an hourly upload cap of 300 files on top of the per-file size limit; going past it returns 409 conflict naming the limit and when it resets.

Clearing out old attachments

Only attachments are removed. Files on clients, quotes, client purchase orders, orders, invoices, purchases, vendors, expenses, projects and accounting exports are in scope, and kinds narrows that; the records themselves are untouched. Catalog item photos and the organization logo are never in scope. The cutoff must be at least 30 days ago, and a cutoff that would take more than 5,000 files or 512 MB is refused with a 422: pick an earlier date. See Your data.

A share link opens the document without an account: read it, sign and accept a quote, follow an order's shipments, or see an invoice's balance with your payment instructions (set in Company Settings → Documents). Its token is 144 random bits and opens that one document only.

A JPEG, PNG, WebP or AVIF is downscaled and re-encoded (at quality 82 for the lossy formats). SVGs and animated GIFs pass through byte for byte, like PDFs. A phone photo of a page typically arrives at 4 MB and is stored under 500 KB, and a file that won't decode is stored exactly as it came. A quote and the order it became show each other's files in the app; the API keeps each file on the record it was uploaded to.

clientPo in a purge covers the purchase orders clients upload through the portal; the submission itself, its status and its declined reason stay.

Statements

What a client owes over a period: computed from the ledger every time, printed, emailed and scheduled.

Method Path What it does
GET /clients/{id}/statement A client's statement of account
POST /clients/{id}/statement/send Email a client their statement
GET /clients/{id}/statement/links A client's statement links
DELETE /clients/{id}/statement/links/{linkId} Revoke a statement link
GET /statements The statements that have actually been sent
GET /settings/statements The automatic statement schedule
PUT /settings/statements Change the schedule

Leaving one client off the automatic run is a client field. See Statements of account.

Leaving one client out of the automatic run is the client field statementsEnabled.

Client portal

A client's own login: who can sign in, the conversation on each document, and which files they see.

Method Path What it does
PATCH /comments/{id} Edit a message you sent the client
DELETE /comments/{id} Take back a message you sent the client
GET /clients/{id}/portal-users Who at this client can sign in
POST /clients/{id}/portal-users Give somebody at this client an account
DELETE /portal-users/{id} Take portal access away
GET /quotes/{id}/comments The client conversation on one quote
POST /quotes/{id}/comments Reply to the client on this quote
GET /orders/{id}/comments The client conversation on one order
POST /orders/{id}/comments Reply to the client on this order
GET /invoices/{id}/comments The client conversation on one invoice
POST /invoices/{id}/comments Reply to the client on this invoice
GET /projects/{id}/comments The client conversation on one project
POST /projects/{id}/comments Reply to the client on this project

Signing in isn't an API operation: a sign-in link goes to the client's inbox. See Client portal.

A portal account is a client's own login: one email address, one client, and everything of that client's (quotes, orders, invoices, credit notes and projects) in one list at /portal/<your workspace>. A sign-in link works once and expires in twenty minutes.

Vendors

Method Path What it does
GET /vendors List vendors
POST /vendors Create a vendor
GET /vendors/{id} Get a vendor (with purchase rollups)
PATCH /vendors/{id} Update a vendor
DELETE /vendors/{id} Delete a vendor
POST /vendors/{id}/archive Archive a vendor
POST /vendors/{id}/restore Restore a vendor

Purchases

Internal purchase orders and outgoing payments.

Method Path What it does
POST /purchases/{id}/duplicate Duplicate a purchase
GET /spending Everything that went out
GET /purchases List purchases
POST /purchases Create a purchase
GET /purchases/{id} Get a purchase (lines and payments)
PATCH /purchases/{id} Update a purchase
DELETE /purchases/{id} Delete a purchase
POST /purchases/{id}/status Set a purchase's status
POST /purchases/reorder Draft purchases from low stock
POST /purchases/{id}/receive Receive everything outstanding (or undo every delivery)
GET /purchases/{id}/receipts Deliveries booked against a purchase
POST /purchases/{id}/receipts Book a delivery in
PATCH /purchases/{id}/receipts/{receiptId} Correct a delivery
DELETE /purchases/{id}/receipts/{receiptId} Undo a delivery
GET /purchases/{id}/pdf The purchase order as a PDF
POST /purchases/{id}/email Email the purchase order to the supplier
GET /purchases/{id}/attachments List the purchase's attachments
POST /purchases/{id}/attachments Upload an attachment to the purchase
GET /vendors/{id}/attachments List the vendor's attachments
POST /vendors/{id}/attachments Upload an attachment to the vendor
POST /purchases/{id}/payments Record an outgoing payment
DELETE /purchases/{id}/payments/{paymentId} Remove a payment

Purchase lines accept itemId (what the line buys), unitOfPurchase and conversionFactor; the header accepts allocateExtraCosts. Receiving a line that names a stock-tracked item adds quantity × conversionFactor to stock at its landed cost, which moves the item's average. See Vendors & purchasing. A purchase's spend category is categoryId, from the same shared list expenses use.

Spending

Read-only. The two records stay two records: receiving a purchase writes a receipt, a stock movement and a new average cost, and an expense never touches stock, so creating and editing either still goes to /purchases or /expenses.

See Spending.

Projects

A piece of work and everything that belongs to it (its quote, its orders, its invoices, the material, the hours and the expenses) plus the answer: did it make money?

Method Path What it does
GET /projects List projects
POST /projects Open a project
GET /projects/{id} Get a project
PUT /projects/{id} Update a project
PATCH /projects/{id} Update a project (alias of PUT)
DELETE /projects/{id} Delete a project
GET /projects/{id}/profitability What a project was quoted at, and what it actually cost
GET /projects/{id}/time-entries List the time logged against a project
POST /projects/{id}/time-entries Log a stint
POST /projects/{id}/bill Bill a project's time, expenses and material onto one invoice
POST /projects/{id}/bill-time Put a project's unbilled time onto an invoice
GET /projects/{id}/bill-stock Material on a project that has not been charged for
POST /projects/{id}/bill-stock Charge a project's material to an invoice
POST /projects/{id}/bill-expenses Re-bill a project's unbilled expenses
GET /projects/{id}/stock What has come off the shelf for a project
POST /projects/{id}/stock Use stock on a project
DELETE /projects/{id}/stock/{movementId} Put stock back on the shelf
GET /time-entries/{id} Get a time entry
PUT /time-entries/{id} Update a time entry
PATCH /time-entries/{id} Update a time entry (alias of PUT)
DELETE /time-entries/{id} Delete a time entry

Quotes, orders, invoices, purchases and expenses accept a projectId, and document lines accept a kind (ITEM, LABOUR, MATERIAL, EXPENSE, TEXT). An ITEM line (the default) needs an itemId, and is refused with a 422 without one. The line on a deposit or progress invoice comes back as DEPOSIT: send it back as it is when you edit that invoice. See Projects.

billRate is the hourly billing rate, a price. Labor cost and bill rate come from the workspace variables wage_per_hour, payroll_tax_percent, insurance_percent, overhead_percent and bill_rate_per_hour; a member can carry their own wage, which only owners and admins can read or set. An ITEM line on a quote, order, invoice or quote template needs an itemId (422 without): a line that isn't a catalog item is sent as the kind it is.

Expenses

The everything-else spend: one amount, one category, a photograph of the receipt, and the shared category list purchases file under too.

Method Path What it does
GET /expenses List expenses
POST /expenses Record an expense
GET /expenses/{id} Get an expense
PUT /expenses/{id} Update an expense
PATCH /expenses/{id} Update an expense (alias of PUT)
DELETE /expenses/{id} Delete an expense
GET /expenses/{id}/attachments List an expense's receipts
POST /expenses/{id}/attachments Add a receipt photograph
POST /expenses/from-image Draft an expense from a photograph
POST /expenses/{id}/confirm Agree with a drafted expense
POST /expenses/{id}/rebill Charge an expense back to a client
POST /expenses/{id}/unbill Un-bill a re-billed expense
GET /expense-categories List the workspace's spend categories
POST /expense-categories Add a spend category
GET /expense-categories/{id} Get a spend category
PUT /expense-categories/{id} Update a spend category
PATCH /expense-categories/{id} Update a spend category (alias of PUT)
DELETE /expense-categories/{id} Delete a spend category
POST /expense-categories/{id}/merge Merge one category into another

A drafted expense is a suggestion: confirmedAt is set once a person agrees with it, and needsReview on each expense says whether anyone has. Spend is counted net of the tax on it (amount - taxAmount) on the spend reports, and with its tax on the tax-time reports. A re-billed line is priced at the net figure plus markupPercent. See Expenses.

A recurring expense writes its next occurrence on the day it falls due.

Accounting

Handing a period's books over: the chart-of-accounts mapping, and the export QuickBooks and Xero import, with a journal that balances every day.

Method Path What it does
GET /accounting/map The chart-of-accounts mapping
PUT /accounting/map Replace one provider's mapping
GET /accounting/exports What has gone over
POST /accounting/exports Generate a period's export
GET /accounting/exports/{id} One export
GET /accounting/exports/{id}/download Download the zip
GET /accounting/handoff The accountant's address and the period reminder
PUT /accounting/handoff Save the accountant's address and the period reminder
GET /accounting/changed-since-export What has moved since the books went over

The zip holds sales, credits, payments, bills, expenses, fees and journal CSVs. The journal balances every day in the period: debits equal credits, checked before anything is written. Generating refuses with 422 unmapped_accounts while anything has no account, listing what is missing; acceptSuspense: true generates anyway into account 9999. See handing the numbers to your accountant.

Imports

Load any of 10 kinds of record from a CSV, and undo a run.

Method Path What it does
POST /imports/preview Parse and validate a file without importing it
GET /imports List import runs
POST /imports Run an import
POST /imports/archive Load a full-export archive back in
GET /imports/{id} Get one import run
GET /imports/{id}/rejected Download the rejected rows
POST /imports/{id}/undo Undo an import

Everything the import wizard does. A file arrives either as multipart/form-data with a file part (plus kind, and optional mapping as a JSON part and duplicatePolicy), as JSON with the CSV in csv, or as already-parsed rows of column → value, for a script with no file to hand. Leave out mapping and the columns are matched by their headings, the way the wizard fills them in. A file can have up to 5,000 rows; more is a 422.

The 10 kinds, roughly in the order to do them: clients, items, materials, vendors, price-list, opening-stock, orders, invoices, payments, expenses. (open-invoices is the old name for invoices and still works.) A preview returns the kind's fields (name, label, type, required, an example and a line of help), so a script can build a file without hard-coding column names.

Rows are created through the same services as the rest of the API, so validation, numbering and the activity trail are identical; a rejected row never stops the run. Each batch reports createdCount, updatedCount, skippedCount and errorCount, and keeps createdIds, the undo list. Undo covers created records only; rows that updated an existing record aren't rewound.

Two things to know before you script against it. opening-stock creates nothing: it sets balances, so importing a corrected count books the difference rather than doubling the shelf, and undo leaves it alone. And imported history is never counted: opening-balance invoices don't touch the free 50, because the sales happened before OrderDen.

A CSV this API produced maps itself. The headers a list export writes are matched against the import's fields before the synonym table (which also covers a webstore's products export column for column), so GET /clients?format=csv piped back into POST /imports needs no mapping. Columns worked out rather than stored (Margin %, Balance, Days overdue, Created, Updated, Id) are recognized and not read back; custom-field columns map by their own labels. Import the kinds in the order listed: each depends only on the ones before it.

What counts as a duplicate is each kind's own identity:

Kind Matches on
clients Name plus the first email
items SKU, or item number and revision
materials Material number, then name
vendors Name
price-list The price list plus the item or variant
opening-stock The item (or variant) and the location
invoices Client plus invoice number
payments The payment number, else what it settled plus the date and amount
expenses Date, amount and vendor

An item row with no number takes its SKU as its item number, which is what makes a webstore export import cleanly. A row with neither is refused, naming Item #.

Where a kind departs from the policy. invoices refuses update per row (an invoice with a payment against it can't have its total rewritten by an import: correct it with a credit note), and under create takes a fresh number when the file's is taken, keeping the original in the notes. payments refuses update too: remove the payment and import the row again, or refund it. A payment's identity is also what stops an invoices file and a payments file booking the same money twice.

What each kind writes. clients writes the client, its contact methods and up to two addresses: one set of address columns becomes the default for both billing and shipping, two different ones are written as two addresses, and two identical ones collapse to one. otherContacts takes further channels as TYPE: value (label) separated by semicolons (EMAIL, PHONE, MOBILE, FAX, WEBSITE, OTHER); an untyped value is an email if it contains @ and a phone otherwise. Under update, an address is matched on its street and rewritten in place, a new street is added beside the others, and nothing is removed.

materials writes an item of type MATERIAL with stock tracking on. The unit is found by code and never created (an unknown code is a row error naming the codes that exist); the category is found by name and created when missing. openingQuantity books one opening movement, as POST /inventory/opening-balances does. opening-stock turns tracking on for an item that had it off.

invoices writes each row as an opening balance, so it ages on the debtors report and never counts as revenue again, then settles it: amountPaid becomes a real payment on paidDate with paymentMethod, so a settled invoice lands PAID with a payment behind it. status covers only what a payment can't say: DRAFT, VOID and WRITTEN_OFF are applied afterward, and PARTIALLY_PAID and OVERDUE fold onto SENT, the default.

payments takes allocations ("2026-0184: 620.00; 2026-0191: 380.00") to settle several invoices with one payment. A row with no invoice is money on account and needs a client, and anything over what the invoices owe becomes that client's credit rather than a rejected row. fee is written with the net that follows from it.

expenses creates the category and the vendor it needs, but never a project: an unknown project number leaves the expense unattached.

# Preview a products export, then import it
curl -s -X POST $BASE/imports/preview -H "$AUTH" \
  -F kind=items -F file=@products_export.csv | jq '.data | {rowCount, newCount, errorCount}'

BATCH=$(curl -s -X POST $BASE/imports -H "$AUTH" \
  -F kind=items -F duplicatePolicy=update -F file=@products_export.csv | jq -r .data.id)

# Changed your mind
curl -s -X POST $BASE/imports/$BATCH/undo -H "$AUTH" | jq .data.archivedCount

# Moving in: records, then the shelf, then the history. Each file is its own
# run, and each one can be undone on its own.
for kind in clients items materials vendors; do
  curl -s -X POST $BASE/imports -H "$AUTH" -F kind=$kind -F file=@$kind.csv \
    | jq -r '"\(.data.kind): \(.data.createdCount) created, \(.data.errorCount) rejected"'
done
curl -s -X POST $BASE/imports -H "$AUTH" \
  -F kind=opening-stock -F [email protected] | jq '.data | {updatedCount, errorCount}'

# The sales history: invoices first (settled ones bring their payment with
# them), then any payment that settled more than one invoice at a time.
curl -s -X POST $BASE/imports -H "$AUTH" \
  -F kind=invoices -F [email protected] | jq '.data | {createdCount, errorCount}'
curl -s -X POST $BASE/imports -H "$AUTH" \
  -F kind=payments -F [email protected] | jq '.data | {createdCount, skippedCount}'

# What in this file do I already have?
curl -s -X POST $BASE/imports/preview -H "$AUTH" \
  -F kind=clients -F [email protected] \
  | jq '.data.duplicates[] | {row, existingLabel, matchedOn}'

Settings

Org-wide template, sales and mail settings, and per-member notifications.

Method Path What it does
GET /settings/late-fees The workspace's late-fee rule
PUT /settings/late-fees Change the late-fee rule
GET /settings/calendar Members who have connected a calendar, and whether each is up to date
POST /settings/calendar/sync Copy the order dates to every connected calendar now
GET /settings/storage Storage providers this workspace has connected
PUT /settings/storage Choose where new attachments are written
GET /settings/storage/{provider} One storage provider's connection
DELETE /settings/storage/{provider} Forget a storage connection
POST /settings/storage/{provider}/connect Start connecting a workspace's own storage
GET /export Download everything in the workspace as a zip
GET /settings/numbering Number formats and next numbers
PUT /settings/numbering Set next numbers
POST /renumber Correct a document's number
GET /settings/templates Template settings (theme, footer, number formats)
PUT /settings/templates Replace template settings
GET /settings/templates/designs The document designs and color palettes on offer
GET /settings/sales Sales policy (minimum gross margin)
PUT /settings/sales Update the sales policy
GET /suppressions Addresses this workspace has stopped emailing
POST /suppressions Stop emailing an address
DELETE /suppressions/{id} Start emailing an address again
GET /settings/notifications A member's notification preferences
PUT /settings/notifications Save a member's notification preferences
GET /settings/mail Email signature and blind copy
PUT /settings/mail Replace the email signature and blind copy
DELETE /settings/mail Clear the email signature and blind copy
GET /settings/payments Both payment processors, and what the pay page offers
PUT /settings/payments One processor's options: methods offered, and whether part payments are allowed
GET /settings/mail/oauth The mailbox connected over OAuth
DELETE /settings/mail/oauth Forget the connected mailbox
POST /settings/mail/oauth/connect Start connecting a mailbox
POST /settings/mail/oauth/verify Send a test email through the connected mailbox
PUT /settings/payments/offer Which connected processors the pay page offers by default
POST /settings/payments/connect Start connecting a payment account
DELETE /settings/payments/connect Disconnect one payment processor

Connecting a third-party account (mail, payments, storage or calendar) happens in a browser. A connect endpoint returns a url for a person to open in a browser already signed in to OrderDen; your job is to fetch it and hand it over. The callback checks a one-time token against an HttpOnly cookie and against the signed-in session's workspace, and an API key can supply neither. OrderDen doesn't accept a refresh token or an authorization you got yourself: consent is given by a person. A connect endpoint returns a 503 not_configured when the server has no OAuth client for that provider. Disconnecting mail, storage and payments needs no browser; a calendar is disconnected in the app.

GET/PUT /settings/notifications reads and saves what reaches one member, in the app, by email and by push. Preferences are per member and a key acts as the whole workspace, so userId names the member (a query parameter on GET, in the body on PUT): { userId, preferences: [{ kind, email, inApp, push }] }. A kind left out keeps its value.

GET/PUT /settings/templates holds the document settings. PUT replaces the whole record: themeId is required, a text field you leave out is cleared (and so is orderLeadTimeDays), and requireSignatureByDefault left out becomes true, so read, change and send it all back. It holds the document design and its major and minor accent colors; letterheadLayout (beside, stacked or logo-only: where the logo sits against your name and address, beside by default, and left out keeps what is set); the footer, plus quoteFooter, invoiceFooter and purchaseFooter (blank uses the shared one); the terms new documents start with, defaultTermsText (quotes; blank prints none, as there is no built-in set), invoiceTermsText (blank prints the quote terms) and purchaseTermsText (blank prints nothing); signatureConsentText and requireSignatureByDefault; payment instructions; the number formats for quotes, orders, invoices, purchases, payments, production runs and items; quoteValidityDays, how many days a new quote stays open (30 unless you change it, 0 for none); orderLeadTimeDays, the default lead time a new order's promised date starts on; the portal's portalWelcomeText, portalPaidMessage (blank shows nothing) and portalPoEnabled; and the packing-slip fields: packingSlipThankYou (Markdown with {{client.firstName}}, {{order.number}}, {{org.name}} and {{org.website}}), packingSlipShowPrices, packingSlipShowGiftMessage, packingSlipShowBins, packingSlipSize (A4, LETTER or LABEL_4X6), packingSlipSocialHandle and packingSlipReviewUrl. GET /settings/templates/designs lists the 15 designs and the two color palettes the pickers offer, so you can find a valid themeId rather than guessing one.

GET/PUT /settings/sales holds minimumMarginPercent (the gross-margin floor that flags an order on confirm; null clears it, and leaving it out of a save keeps it); defaultTaxGroupId, the tax a new line falls back to; blockNegativeStock, which makes a production run that's short of a material refuse rather than warn; lowStockBasis (ON_HAND or AVAILABLE), what the reorder point is measured against; quoteFlow, whether an accepted quote becomes an order, an invoice, or a question each time; and the autoEmailOnConfirm, autoEmailOnShip and autoEmailOnDeliver switches for the order status emails.

GET/PUT/DELETE /settings/mail is your email signature and blind copy: signature ends every templated email and bccAll copies the automatic ones, and GET returns null when neither has been saved. There is no way to give OrderDen a mail server and password; to send from your own address, connect a mailbox.

GET/POST /suppressions and DELETE /suppressions/{id} are the addresses OrderDen has stopped emailing: they bounced permanently, someone reported the mail as spam, or they bounced temporarily 3 times within 90 days with nothing getting through in between (SOFT_BOUNCE). Sending to one fails naming the address rather than disappearing into the provider. A single temporary bounce never lands here, and a successful delivery resets the count. Clearing one also clears it at the mail provider, whose own list is account-wide and would otherwise keep rejecting the address for a reason you can't see.

GET /settings/mail/oauth is the mailbox connected over OAuth instead: the address, status, canSend and lastError, or null when there is none, and never a token. A live one is used ahead of OrderDen's own delivery. DELETE forgets it; POST /settings/mail/oauth/verify with { "to": … } sends a test through it and never falls back to another sender, so a 422 mailbox_error answers whether the mailbox still works. POST /settings/mail/oauth/connect takes { "provider": "GOOGLE" } or { "provider": "MICROSOFT" } (Google when you send neither) and returns a url for a person to open in a browser. A workspace sends as one identity, so asking for one provider while the other is connected returns a 409: disconnect the mailbox first.

GET /settings/calendar lists the members who have connected their own Google Calendar: userId, email, calendarName, hasCalendar, status, canWrite, lastSyncedAt and lastError, never a token. It's worth watching: the calendars are per member, so a grant revoked at Google leaves one person reading a schedule that stopped updating and still looks current. There's no connect endpoint; consent happens inside that member's own Google account, in a browser.

POST /settings/calendar/sync copies the order dates to every connected calendar now instead of at the next quarter hour, and answers { "queued": true }. false means nobody has a calendar connected. A second call within a minute does nothing extra.

GET /settings/storage lists the storage providers this workspace has connected its own account on: provider, slug, accountLabel, folderName, status, canStore, isWriteTarget and lastError, never a token. An empty list means OrderDen keeps the files, which is the default. PUT /settings/storage with { "writeTo": "GOOGLE_DRIVE" } (or "DROPBOX", or null for OrderDen's own storage) chooses where new attachments are written. Nothing moves: it changes the next upload and nothing before it. A workspace may connect both providers, and each file opens from wherever it was written. GET and DELETE /settings/storage/{provider} read and forget one of them, where {provider} is the entry's slug (google-drive or dropbox, never the upper-case value, which lives in bodies). Disconnecting deletes nothing, because the files are in the workspace's own storage, but attachments held there stop opening until it's connected again. POST /settings/storage/{provider}/connect returns a url for a person to open in a browser.

GET /settings/payments returns offer (STRIPE, PAYPAL or BOTH: what the pay page offers by default), allowPartialOnlinePayment (whether a client may pay part of the balance, whichever processor they pay through), offered (what it actually offers now, after falling back when the chosen processor can't take payments), and processors: one { provider, available, account } per processor, the same shape as the shipping settings. account has accountId (null while not connected), onboardingComplete, chargesEnabled, payoutsEnabled, canAcceptPayments and Stripe's acceptCards and acceptBankDebit toggles. PUT /settings/payments sets one processor's toggles and needs provider. PUT /settings/payments/offer with { "offer": … } sets the default, and an optional allowPartialOnlinePayment beside it turns part payments on or off for every processor at once. An invoice overrides it with paymentOffer on PATCH /invoices/{id} (null follows the workspace). POST /settings/payments/connect with { "gateway": "STRIPE" } or "PAYPAL" returns a url to open in a browser, where you approve OrderDen against the account you already have. Connecting one leaves the other as it is. DELETE /settings/payments/connect?provider=STRIPE disconnects that one (204, safe to repeat). See Getting paid online.

Your data (full export)

GET /export streams a .zip of the workspace: CSV spreadsheets of the clients, items, quotes, orders, invoices, payments, vendors and purchases lists, JSON of the full records (lines, tax, shipments, payments, contacts, production runs, stocktakes, time entries, settings, the activity trail) and every uploaded file as it was uploaded. Credentials and live connections are left out; Your data lists what is in it. The archive is built once every 30 days and kept until then: GET /export again inside that window returns the same file, so a download that dropped can be repeated. GET /export?check=1 reports { available, lastExportAt, nextAvailableAt, stored } without building anything. stored is true while there is an archive to download again.

Individual lists still export as CSV any time with ?format=csv.

Connecting an account has no way around the browser: we don't accept a refresh token or an authorization you obtained yourself, and there's no one-time token an API client can redeem for a connected account. Disconnecting needs no browser and is idempotent.

Your data in and out. Everything that comes out goes back in, or says why not:

Export Comes back through Why
GET /clients?format=csv POST /imports kind=clients Carries the address book: both addresses, every contact method, terms and notes
GET /items?format=csv kind=items or kind=materials Stock balances come back through kind=opening-stock or the archive
GET /invoices?format=csv kind=invoices Total, dates, what was paid and when and how, and the status. Lines need the archive
GET /payments?format=csv kind=payments Including Allocated to, so one payment across three invoices stays one payment
GET /vendors?format=csv kind=vendors Column for column
GET /inventory?format=csv kind=opening-stock On hand becomes a counted balance; a recount books the difference
GET /expenses?format=csv kind=expenses Column for column
GET /export (the zip) POST /imports/archive The whole workspace
Quotes, orders, purchases and credit notes as CSV POST /imports/archive A list row is a total and the document is its lines, so only the archive brings one back whole
Projects and the stock ledger POST /imports/archive They hold records that already exist
GET /spending?format=csv Nothing Every row already belongs to a purchase or an expense, so importing it would book each spend twice
GET /reports/*?format=csv Nothing Worked out from records. Load the records and the report follows
GET /accounting/exports/{id}/download Nothing Reading one back would book the same period twice

Payment terms

Named terms with deposits, the installments agreed for a project, and the invoices they produce.

Method Path What it does
GET /payment-terms The workspace's named payment terms
POST /payment-terms Add a payment term
GET /payment-terms/{id} One payment term
PUT /payment-terms/{id} Replace a payment term
DELETE /payment-terms/{id} Remove a payment term
GET /orders/{id}/schedule The installments agreed for this project
PUT /orders/{id}/schedule Replace the schedule
POST /orders/{id}/schedule/{stepId}/invoice Invoice one stage of the schedule
POST /orders/{id}/prepayment-invoice Take payment for an order before it has been billed
POST /orders/{id}/final-invoice Close the project out
POST /invoices/combined One invoice for several of a client's orders

GET/POST /payment-terms and GET/PUT/DELETE /payment-terms/{id} manage the workspace's named terms. A term says what is wanted up front (a percentage or a fixed amount, never both) and when the balance falls due (ON_INVOICE, NET_DAYS, ON_DELIVERY or ON_COMPLETION; the last two are events, so an invoice raised under them starts without a due date).

Quotes accept paymentTermId, depositRequired, depositPercent and depositFixed, and report depositAmount. Accepting a quote that requires a deposit above zero raises a DEPOSIT invoice for it, once.

GET/PUT /orders/{id}/schedule reads and replaces the installments agreed for a project; steps: [] removes the schedule (204). A schedule made entirely of percentages must add up to 100; mix in a fixed amount and that check stops applying, but fixed amounts over the contract are a 422. A PUT can't drop a step that has been invoiced (409), and invoiced steps are matched by position, so send them first and unchanged. POST /orders/{id}/schedule/{stepId}/invoice turns a stage into its own PROGRESS invoice. The money a schedule reports (contractTotal, scheduled, invoiced, remainingToInvoice and each step's amount and value) comes back as decimal strings like every other amount; percent stays a number.

POST /orders/{id}/prepayment-invoice takes money for an order before anyone has billed for it: a DEPOSIT invoice for what is left of the order, which the final invoice later deducts rather than billing twice. It's idempotent on the order, and it's what the portal's pay button on an order link calls.

POST /orders/{id}/final-invoice closes the project out: the whole contract, less every deposit and progress payment already paid on it, deducted as its own negative line that takes the deposit's tax back off with it. Invoices expose kind and appliedDeposits; GET /invoices/{id} also returns balance.

POST /invoices/combined takes { orderIds, status? } and closes several of one client's orders out on a single invoice. Each order's lines, shipping and paid deposits are worked out on their own and then added up, so the combined bill charges exactly what the separate ones would have. order is null on it and billedOrders names each order with its own share of the total; those shares are what each order's invoiced figure counts, so five orders on one bill are each counted once.

It refuses (422) where the orders can't share a document, naming them where it can: a whole-document discount (a discount on the lines is fine), a payment schedule, different projects, different currencies, or shipping at two different tax groups. A combined invoice counts as one against the Free plan's lifetime 50, because it's one invoice.

For a whole list at once, POST /orders/bulk with action: "invoice" and payload.combine: true does the same per client: a client with one order in the selection gets an ordinary invoice, and a group that can't be combined fails every order in it with the reason while the other clients' groups go through.

See Deposits, installments and final invoices.

Accepting a quote that requires a deposit, through the share link or by setting its status, raises a DEPOSIT invoice for the amount at the quote's own tax rate.

# Half up front, the rest on delivery
TERM=$(curl -s -X POST $BASE/payment-terms -H "$AUTH" -H 'content-type: application/json' -d '{
  "name": "50% deposit, balance on delivery",
  "balanceDueOn": "ON_DELIVERY",
  "depositPercent": 50,
  "description": "50% deposit on acceptance; the balance is payable on delivery."
}' | jq -r .data.id)

# A quote written under it asks for the deposit without being told twice
curl -s -X POST $BASE/quotes -H "$AUTH" -H 'content-type: application/json' -d "{
  \"clientId\": \"$CLIENT\", \"billTo\": \"…\", \"shipTo\": \"…\",
  \"paymentTermId\": \"$TERM\",
  \"lines\": [{ \"description\": \"Fitted wardrobe\", \"quantity\": 1, \"unitPrice\": 4200 }]
}" | jq '.data | {number, depositRequired, depositAmount}'

# …and closing the project out deducts what was paid
curl -s -X POST $BASE/orders/$ORDER/final-invoice -H "$AUTH" \
  | jq '.data | {number, kind, appliedDeposits, total}'

Reminders

Automatic chasing: the schedule, the editable email templates it sends, and the per-document pause.

Method Path What it does
GET /settings/reminders The workspace's chasing schedule
PUT /settings/reminders Replace the chasing schedule
GET /email-templates Every email template
GET /email-templates/{key} One email template
PUT /email-templates/{key} Save the workspace's own wording
DELETE /email-templates/{key} Reset to the built-in wording
POST /email-templates/{key}/test Send yourself a specimen
GET /invoices/{id}/reminders When this invoice is next chased, and what has gone already
PUT /invoices/{id}/reminders Pause or resume chasing this invoice
POST /invoices/{id}/reminders/send Send a payment reminder now
GET /quotes/{id}/follow-ups When this quote is next followed up, and what has gone already
PUT /quotes/{id}/follow-ups Pause or resume following this quote up
POST /quotes/{id}/follow-ups/send Send a follow-up now

GET/PUT /settings/reminders reads and updates the chasing schedule: the steps for invoices and quotes, the send hour (local to the workspace), skipWeekends, the master pauseAll, and the daily summary's digestEnabled and hour. A field you leave out keeps its value. A step is { key, offsetDays, enabled, templateKey }. Keep a step's key when you edit it: a step counts as sent only when its key is in the reminder log, so a renamed key can chase the client again.

GET /email-templates lists every letter the app sends on your behalf; GET/PUT/DELETE /email-templates/{key} reads, replaces and resets one, and POST /email-templates/{key}/test with { "to": … } sends a specimen filled in with a sample client and invoice. The body is Markdown with {{…}} variables; one the document doesn't carry renders blank.

GET/PUT /invoices/{id}/reminders and GET/PUT /quotes/{id}/follow-ups report when the next one is due and everything already sent, and pause chasing on that one document. POST .../reminders/send and POST .../follow-ups/send send one immediately, recorded under its own manual-… key, so a scheduled step is never used up by it.

Every send is recorded against (document, step), so a client is never chased twice for the same step however many times the worker runs. Chasing stops by itself when the invoice is settled or the quote answered.

See Reminders, follow-ups and status emails.

Notifications

One member's in-app notification feed.

Method Path What it does
POST /me/push-subscriptions Subscribe this browser to web push
DELETE /me/push-subscriptions Unsubscribe this browser from web push
GET /notifications One member's bell menu
DELETE /notifications Clear a member's bell
DELETE /notifications/{id} Clear one notification
PUT /notifications/{id}/read Mark one notification read (or unread)

GET /notifications?userId=… is one member's bell menu: { items: [{ id, kind, title, body, href, readAt, createdAt }], unread }, newest first, with unreadOnly and limit. PUT /notifications/{id}/read with { userId } marks a row read (or unread with read: false). DELETE /notifications/{id}?userId=… clears one row from that member's bell and DELETE /notifications?userId=… clears all of them, read and unread. Both answer 204.

Push notifications

See OrderDen on your phone.

Worked example

BASE=https://your-domain.example/api/v1
AUTH="Authorization: Bearer $ORDERDEN_API_KEY"

# Create a client, quote them, accept, convert to an order
CLIENT_ID=$(curl -s -X POST $BASE/clients -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "Jane Doe", "companyName": "Acme"}' | jq -r .data.id)
QUOTE_ID=$(curl -s -X POST $BASE/quotes -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"clientId\": \"$CLIENT_ID\", \"billTo\": \"Acme, 1 Main St\", \"shipTo\": \"Acme, 1 Main St\", \"lines\": [{\"description\": \"Widget\", \"quantity\": 3, \"unitPrice\": 25}]}" | jq -r .data.id)
curl -s -X POST $BASE/quotes/$QUOTE_ID/status -H "$AUTH" -H "Content-Type: application/json" -d '{"status": "ACCEPTED"}' > /dev/null
ORDER_ID=$(curl -s -X POST $BASE/quotes/$QUOTE_ID/convert -H "$AUTH" | jq -r .data.id)

# Invoice it and record the payment
INVOICE=$(curl -s -X POST $BASE/orders/$ORDER_ID/invoice -H "$AUTH")
INVOICE_ID=$(echo "$INVOICE" | jq -r .data.id)
curl -s -X POST $BASE/invoices/$INVOICE_ID/payments -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"amount\": $(echo "$INVOICE" | jq -r .data.total), \"method\": \"CARD\"}"

For AI assistants

/llms.txt is a map of this API and the guides, in the format an assistant looks for first; /llms-full.txt is every endpoint and event in one file, for one with room to read it.

OrderDen's MCP server isn't published yet. Until it is, an assistant can use the API directly with a key and the OpenAPI spec. It gets the permissions of the key you give it and the same per-minute rate limit, so give it a key made by someone whose access matches what you want it doing: a key that can't see costs gives it an assistant that can't see costs.

Both files are generated from this API's description and the guide, so neither goes stale.

Zapier, Make and n8n. Our own Zapier app isn't published yet; until it is, a Zap connects through Webhooks by Zapier. Make and n8n need no app: both drive plain webhooks in and REST calls out with their generic modules. Automation covers all three.

Everything on this page is on the free tier: one person, the whole product, no card.

Start free