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.pageSizedefaults 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,/webhooksand/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=fieldor?sort=field:ascorfield:desc(such assort=total:desc). Each has its own set of sortable fields, and an unknown one is a 400 whosedetailsnames the valid ones. Without a direction, text sorts A→Z and dates and amounts newest or largest first. Other lists ignoresort. - 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 whateverformatsays: 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.limitisinvoicesorseats), 403 forbidden (the key's permissions don't cover this;details.permissionnames the flag;no_seatwhen 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
detailsis{ "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 newestupdatedAtyou 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-Keyheader (any unique string) on a POST, PUT, PATCH or DELETE. Retrying with the same key within 24 hours replays the first successful response, markedIdempotent-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-RemainingandX-RateLimit-Reset(unix seconds). A 429 carriesdetails.limitanddetails.resetAtin 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'sonHandandallocated, are decimal strings. Dates are ISO 8601. The one exception is a carrier network's own price: a shipping rate'samountand a bought label'samountare plain numbers, as the network quoted them. - Updates: documents use
PATCH; most catalog and settings records usePUT. Either way only the fields you send change, except where a section says aPUTreplaces the whole record. - Numbers: document numbers come from your configured formats when
numberis left out. Quotes, orders, invoices, purchases and payments accept one of your own on create, andPATCHrenames 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:
linesmay 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 aPATCHsendslines, send those ids back with them: a line carrying itsidis 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. Anidthat isn't a line on that document, or the same one sent twice, is a 422.idis ignored onPOST, so a document read back and posted as a copy needs nothing stripped. - Discounts: documents and their lines take an optional
discountType(PERCENTorAMOUNT) with adiscountValue, both or neither;discountType: nullonPATCHclears it. Line discounts come offquantity × unitPrice, the document discount offsubtotalbefore tax, and responses add the resolveddiscountAmountat 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 (nullwhen unknown). It's snapshotted on create and wheneverlinesare replaced, and carried unchanged through convert, invoice and duplicate. A line with noitemIdmay setunitCostitself; 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 plusshippingAmount, tax excluded) against the cost basis (each line'squantity × unitCost, every recorded shipmentcost, the payment processor's fees and the expenses linked to the order, net of tax). It carriesrevenue,lineCost,shipmentCost,paymentFees,expenseCost,costBasis,marginandmarginPercent, per line underlines, withcoveragesaying how many lines and shipments have a cost. A cost in another currency than the order's is left out and counted incoverage.leftOut. A line or shipment with no cost is unknown, never zero: it's left out, andmarginisnullwhen no line has one. - Sales documents require
billToon create (andshipTotoo on an order; a quote or invoice can leave it out) and carrypublicNotes(client-facing) andnotes(internal), plus an optionalcustomerPo(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) andpromisedDate(what you committed to). Both are optional dates, cleared by sendingnull, andpromisedDatedrives 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.
Print batches
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
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