Every operation, grouped as the explorer above 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.
Health
Whether the deployment is up, and this API's own description. No key needed.
Custom fields & bulk
Fields a workspace adds to its own records, and doing one thing to many records at once.
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.
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.
Webhooks
Being told when something happens instead of asking: signed deliveries, retries and the delivery log.
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.
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.
Reports
Revenue, margin and fulfillment reports over a date range.
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.
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.
Stock
Quantity on hand from the movement ledger, adjustments, opening balances and low-stock alerts.
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.
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.
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.
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.
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
Quote lines also accept isOptional, isSelected, optionGroup and
optionGroupLabel. The quote's total counts the selected lines alone,
and it exposes optionalTotal (the add-ons on offer and not taken, before
tax), totalsShown and selectionLockedAt. A group of one item at
several quantities prints the item once with its quantities under it, and
until the client picks one for each, totalsShown is false and their copy
shows no totals. 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.
Carriers
The workspace's shipping-carrier list.
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.
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.
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.
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.
See Counting stock.
Stocktakes are numbered like ST-000001.
Barcodes
Turning a scanned code into a thing, and printing the stickers that carry it.
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.
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.
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.
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.
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.
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.
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)
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.
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.
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
Purchases
Internal purchase orders and outgoing payments.
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?
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, labor_overhead_percent and bill_rate_per_hour (recipes use overhead_percent, a different number on a different base); 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.
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.
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.
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:
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.
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 false, 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), packingSlipPouchFold (Letter and
A4 only: folded in quarters, the top-left panel carries the address for a clear
pouch and the lines start below the half fold), 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), accountEmail (the account's email, or null),
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 partial
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:
Payment terms
Named terms with deposits, the installments agreed for a project, and the invoices they produce.
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 created under them starts without a due date).
A quote's deposit is the first step of its payment schedule, the one due
ON_ACCEPT. Quotes accept paymentTermId, depositRequired,
depositPercent and depositFixed as shorthand: they write that step,
with a balance step for the rest, and GET /quotes/{id} reports the result as
deposit and paymentSchedule. Accepting a quote creates a DEPOSIT
invoice for that step, once, and linked to it; converting the quote carries the
step and its invoice onto the order.
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 with a step before its last still to invoice (an order whose deposit
has been invoiced combines), 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, creates 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\": \"Walnut dining table\", \"quantity\": 1, \"unitPrice\": 4200 }]
}" | jq '.data | {number, deposit}'
# …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.
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 hour the daily summary
goes at (who gets it is each person's own DIGEST notification preference).
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 document. 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.
GET /notifications?userId=… is one member's bell menu:
{ items: [{ id, kind, title, body, href, readAt, createdAt }], unread },
newest first, with unreadOnly and limit.
PUT /notifications/{id}/read with { userId } marks a row read (or unread
with read: false).
DELETE /notifications/{id}?userId=… clears one row from that member's bell
and DELETE /notifications?userId=… clears all of them, read and unread.
Both answer 204.
Push notifications
See OrderDen on your phone.