Jaz API Skill
You are working with the Jaz REST API — the accounting platform backend. Also fully compatible with Juan Accounting (same API, same endpoints).
When to Use This Skill
- Writing or modifying any code that calls the Jaz API
- Building API clients, integrations, or data pipelines
- Debugging API errors (422, 400, 404, 500)
- Adding support for new Jaz API endpoints
- Reviewing code that constructs Jaz API request payloads
Quick Reference
Base URL: https://api.getjaz.com
Auth: x-jk-api-key: <key> header on every request — key has jk- prefix (e.g., jk-a1b2c3...). NOT Authorization: Bearer or x-api-key.
Content-Type: application/json for all POST/PUT/PATCH (except multipart endpoints: createBusinessTransactionFromAttachment FILE mode, importBankStatementFromAttachment, and attachment uploads)
All paths are prefixed: /api/v1/ (e.g., https://api.getjaz.com/api/v1/invoices)
Critical Rules
Identifiers & Dates
- All IDs are
resourceId— neverid. References use<resource>ResourceIdsuffix. - All transaction dates are
valueDate— notissueDate,invoiceDate,date. This is an accounting term meaning "date of economic effect." - All dates are
YYYY-MM-DDstrings — ISO datetime and epoch ms are rejected.
Payments (Cross-Currency Aware)
- Payment amounts have two fields:
paymentAmount= bank account currency (actual cash moved),transactionAmount= transaction document currency (invoice/bill/credit note — amount applied to balance). For same-currency, both are equal. For FX (e.g., USD invoice paid from SGD bank at 1.35):paymentAmount: 1350(SGD),transactionAmount: 1000(USD). - Payment date is
valueDate— notpaymentDate, notdate. - Payment bank account is
accountResourceId— notbankAccountResourceId. - Payments require 6 fields:
paymentAmount,transactionAmount,accountResourceId,paymentMethod,reference,valueDate. - Payments wrapped in
{ payments: [...] }— array recommended. Flat objects are now auto-wrapped by the API, but array format is preferred for clarity.
Names & Fields
- Line item descriptions use
name— notdescription. - Item names: canonical field is
internalName, butnamealias is accepted on POST. GET responses return bothinternalNameandname. - Tag names: canonical field is
tagName, butnamealias is accepted on POST. GET responses return bothtagNameandname. - Custom field names: POST uses
name, GET returns bothcustomFieldNameandname. - Invoice/bill number is
reference— notreferenceNumber.
Transaction Creation
saveAsDraftdefaults tofalse— omitting it creates a finalized transaction. Explicitly sendingsaveAsDraft: truecreates a draft.- If
saveAsDraft: false(or omitted), every lineItem MUST haveaccountResourceId. - Phones MUST be E.164 —
+65XXXXXXXX(SG),+63XXXXXXXXXX(PH). No spaces.
Chart of Accounts
- Tax profiles pre-exist — NEVER create them. Only GET and map.
- Bank accounts are CoA entries with
accountType: "Bank Accounts". A convenience endpointGET /bank-accountsexists but returns a flat array[{...}]— NOT the standard paginated{ data, totalElements, totalPages }shape. Normalize before use. - CoA bulk-upsert wrapper is
accounts— notchartOfAccounts. - CoA POST uses
currency— notcurrencyCode. (Asymmetry — GET returnscurrencyCode.) - CoA POST uses
classificationType— GET returnsaccountType. Same values. - CoA code mapping: match by NAME, not code — pre-existing accounts may have different codes. Resource IDs are the universal identifier.
Bulk Upsert (Items, Contacts & Rates)
- Items bulk-upsert (
POST /items/bulk-upsert) — max 500 per call. ProvideresourceIdper item to update (partial — only changed fields needed, server preserves existing values). OmitresourceIdto create (defaults:status=ACTIVE,itemCategory=NON_INVENTORY). Response:{ resourceId: null, resourceIds: [...] }. SYNC — returns resourceIds immediately. - Contacts bulk-upsert (
POST /contacts/bulk-upsert) — max 500 per call. ProvideresourceIdto update (partial), omit to create.billingNamerequired for create. ASYNC — returns{ jobId, status: "QUEUED", totalRecords }. Pollsearch_background_jobswithfilter: {resourceId:{eq:jobId}}until status isSUCCESS,FAILED, orPARTIAL_SUCCESS. Unlike items, contacts bulk-upsert is asynchronous. - Rates bulk-upsert (
POST /organization/currencies/rates/bulk-upsert) — max 500 per call. RequiresrateDirectionper rate (FUNCTIONAL_TO_SOURCEorSOURCE_TO_FUNCTIONAL). Auto-enables currencies not yet enabled in the org — no need to calladd_currencyfirst. Response:{ resourceId: null, resourceIds: [...] }.
Background Jobs (Universal Async Tracking)
- ANY operation that returns a
jobIdcan be polled viasearch_background_jobs. This includes: contacts bulk-upsert (UPSERT_CONTACTS), items bulk-upsert (UPSERT_ITEMS), bank statement import (PROCESS_BANK_STATEMENT_FILES), and magic processing (MAGIC_TRANSACTION_*). - 🚨 CRITICAL: Filter by
resourceId, NOTjobId—filter: {jobId:{eq:...}}is silently ignored (returns ALL jobs). Must usefilter: {resourceId:{eq:theJobId}}. The response field is namedjobIdbut the filter path isresourceId. - Poll until terminal status —
SUCCESS,FAILED, orPARTIAL_SUCCESS. UseprocessedCount,failedCount,totalRecordsfor progress.PARTIAL_SUCCESSmeans some records succeeded and some failed — checkerrorDetailsarray for per-record errors. startedAtfilter does NOT work — usecreatedAtfor date range filtering.errorDetailsis[](empty array) on success — notnull.- Known jobTypes:
UPSERT_CONTACTS,UPSERT_ITEMS,PROCESS_BANK_STATEMENT_FILES,MAGIC_TRANSACTION_PURCHASE,MAGIC_TRANSACTION_SALE,MAGIC_TRANSACTION_SALE_CREDIT_NOTE.
Export Records
outputFormat: "XLSX"is always required — no other format is currently supported. Hardcode it.query+filterare mutually exclusive — the server returnsINVALID_SEARCH_INPUTif both are provided. Passquery(structured search string, same syntax as dashboard) ORfilter(raw JSON filter object), never both.- Available entity types:
INVOICE,BILL,CUSTOMER_CREDIT_NOTE,SUPPLIER_CREDIT_NOTE,SALE_PAYMENT,PURCHASE_PAYMENT,BATCH_PAYMENT,CONTACT,ITEM,CAPSULE,SCHEDULED_TRANSACTION,JOURNAL,BANK_RECORD,CASHFLOW_TRANSACTION,FIXED_ASSET,CHART_OF_ACCOUNT,TAX_PROFILE. fileUrlexpires in ~5 minutes — it's a pre-signed S3 URL. Warn the user to download immediately.- Preview first — use
preview_export_recordsto confirm scope (count + sample rows) before callingexport_records. ThefilterDescriptionfield gives a human-readable summary like"2580 records | Status in: UNPAID". - Column customization — use
get_export_columnsto discover available column paths and headers. Pass acolumnsarray to select specific fields. Omit for default columns. previewRowskeys are column headers — not field paths. E.g.{"Invoice Ref #": "INV-001", "Customer": "Acme"}. UseresolvedColumnsto map headers back to paths.
Journals & Cash
- Journals use
journalEntrieswithamount+type: "DEBIT"|"CREDIT"— NOTdebit/creditnumber fields. - Journals support multi-currency via
currencyobject — same format as invoices/bills:"currency": { "sourceCurrency": "USD" }(auto-fetch platform rate) or"currency": { "sourceCurrency": "USD", "exchangeRate": 1.35 }(custom rate). Must be enabled for the org. Omit for base currency. Three restrictions apply to foreign currency journals: (a) no controlled accounts — accounts withcontrolFlag(AR, AP) are off-limits (use invoices/bills instead), (b) no FX accounts — FX Unrealized Gain/Loss/Rounding are system-managed, (c) bank accounts must match — can only post to bank accounts in the same currency as the journal (e.g., USD journal → USD bank account only, not SGD bank account). All other non-controlled accounts (expenses, revenue, assets, liabilities) are available. currencyobject is the SAME everywhere — invoices, bills, credit notes, AND journals all usecurrency: { sourceCurrency: "USD", exchangeRate?: number }. Never usecurrencyCode: "USD"(silently ignored on invoices/bills) orcurrency: "USD"(string — causes 400 on invoices/bills).- Cash entries use
accountResourceIdat top level for the BANK account +linesarray for offsets.
Credit Notes & Refunds
- Credit note application wraps in
creditsarray withamountApplied— not flat. - CN refunds use the same Payment shape as invoice/bill payments —
paymentAmount,transactionAmount,accountResourceId,paymentMethod,valueDate,reference. The API also accepts aliasesrefundAmount/refundMethod(see Rule 53) but prefer canonicalpaymentAmount/paymentMethodfor consistency.
Inventory Items
- Inventory items require:
unit(e.g.,"pcs"),costingMethod("FIXED"or"WAC"),cogsResourceId,blockInsufficientDeductions,inventoryAccountResourceId.purchaseAccountResourceIdMUST be Inventory-type CoA. - Delete inventory items via
DELETE /items/:id— not/inventory-items/:id.
Cash Transfers
- Cash transfers use
cashOut/cashInsub-objects — NOT flatfromAccountResourceId/toAccountResourceId. Each:{ accountResourceId, amount }.
Schedulers
- Scheduled invoices/bills wrap in
{ invoice: {...} }or{ bill: {...} }— not flat. Recurrence field isrepeat(NOTfrequency/interval).saveAsDraft: falserequired.referenceis required inside theinvoice/billwrapper — omitting it causes 422. - Scheduled journals use FLAT structure with
schedulerEntries— not nested injournalwrapper.valueDateis required at the top level (alongsidestartDate,repeat, etc.).
Bookmarks
- Bookmarks use
itemsarray wrapper withname,value,categoryCode,datatypeCode.
Custom Fields
- Do NOT send
appliesToon custom field POST — causes "Invalid request body". Only sendname,type,printOnDocuments. 35a. Custom field values on transactions: Set viacustomFields: [{ customFieldName: "PO Number", actualValue: "PO-123" }]on invoice/bill/customer-CN/supplier-CN/payment/item/fixed-asset create/update. NOT on journals, cash entries, or cash transfers. Read from GET responses in the same shape. 35b. Custom field search:POST /custom-fields/searchwith filter/sort/limit/offset. Filter bycustomFieldName(StringExpression),datatypeCode(StringExpression: TEXT, DATE, DROPDOWN). 35c. Custom field GET:GET /custom-fields/:resourceIdreturns full definition includingapplyToSales,applyToPurchase,applyToCreditNote,applyToPayment,printOnDocuments,listOptions.
Tags on Transactions
35d. Tags are tags: string[] on ALL transaction create/update: invoices, bills, customer CNs, supplier CNs, journals, cash-in, cash-out, cash transfers. CLI uses --tag <name> (singular, wrapped to array). API accepts the array directly.
Nano Classifiers
35e. ClassifierConfig on line items: classifierConfig: [{ resourceId: "<capsuleTypeId>", type: "invoice"|"bill", selectedClasses: [{ className: "Class A", resourceId: "<classId>" }], printable: true }]. Applies to line items on invoices, bills, credit notes, journal entries, and cash entry details. Create capsule types first via POST /capsule-types, then reference them in classifierConfig.
Reports
- Report field names differ by type — this is the most error-prone area:
| Report | Required Fields |
|---|---|
| Trial balance | startDate, endDate |
| Balance sheet | primarySnapshotDate |
| P&L | primarySnapshotDate, secondarySnapshotDate |
| General ledger | startDate, endDate, groupBy: "ACCOUNT" (also TRANSACTION, CAPSULE) |
| Cashflow | primaryStartDate, primaryEndDate |
| Cash balance | reportDate |
| AR/AP report | endDate |
| AR/AP summary | startDate, endDate |
| Bank balance summary | primarySnapshotDate |
| Equity movement | primarySnapshotStartDate, primarySnapshotEndDate |
| Ledger highlights | (none — simple GET) |
- Ledger highlights is a simple GET —
GET /api/v1/ledger/highlightsreturns org-wide GL summary metadata: transaction counts by type, date range, active accounts/currencies, cross-currency flag, and dynamic FX types. No parameters. Response dates are epoch ms (see Rule 52). 37a. Data exports use simpler field names: P&L export usesstartDate/endDate(NOTprimarySnapshotDate). AR/AP export usesendDate.
Pagination
- All list/search endpoints use
limit/offsetpagination — NOTpage/size. Default limit=100, offset=0. Max limit=1000, max offset=65536.page/sizeparams are silently ignored. Response shape:{ totalPages, totalElements, truncated, data: [...] }. Whentruncated: true, a_meta: { fetchedRows, maxRows }field explains why (offset cap or--max-rowssoft cap — default 10,000). Use--max-rows <n>to override. Always checktruncatedbefore assuming the full dataset was returned.
Other
- Currency rates use
/organization-currencies/:code/rates— note the HYPHENATED path (NOT/organization/currencies). Enable currencies first viaPOST /organization/currencies, then set rates viaPOST /organization-currencies/:code/rateswith body{ "rate": 0.74, "rateApplicableFrom": "YYYY-MM-DD" }(see Rule 49 for direction). Cannot set rates for org base currency. Full CRUD: POST (create), GET (list), GET/:id, PUT/:id, DELETE/:id. - FX invoices/bills MUST use
currencyobject —currencyCode: "USD"(string) is silently ignored (transaction created in base currency!). Usecurrency: { sourceCurrency: "USD" }to auto-fetch platform rate (ECB/FRANKFURTER), orcurrency: { sourceCurrency: "USD", exchangeRate: 1.35 }for a custom rate. Rate hierarchy: org rate → platform/ECB → transaction-level. - Invoice GET uses
organizationAccountResourceIdfor line item accounts — POST usesaccountResourceId. Request-side aliases resolveissueDate→valueDate,bankAccountResourceId→accountResourceId, etc. - Scheduler GET returns
interval— POST usesrepeat. (Response-side asymmetry remains.) - Search sort is an object —
{ sort: { sortBy: ["valueDate"], order: "DESC" } }. Required whenoffsetis present (evenoffset: 0). - Bank records — Create: Multipart CSV/OFX via
POST /magic/importBankStatementFromAttachmentor JSON viaPOST /bank-records/:accountResourceIdwith{ records: [{amount, transactionDate, description?, payerOrPayee?, reference?}] }(positive = cash-in, negative = cash-out, response:{data: {errors: []}}). Search:POST /bank-records/:accountResourceId/search— filter fields:valueDate(DateExpression),status(StringExpression: UNRECONCILED, RECONCILED, ARCHIVED, POSSIBLE_DUPLICATE),description,extContactName(payer/payee),extReference,netAmount(BigDecimalExpression),extAccountNumber. Sort byvalueDateDESC default. - Withholding tax on bills/supplier CNs only. Retry pattern: if
WITHHOLDING_CODE_NOT_FOUND, strip field and retry. - Known API bugs (500s): Contact groups PUT (nil pointer on search response), custom fields PUT (dangling stack pointers in mapping), capsules POST (upstream returns nil), catalogs POST, inventory balances by status GET (
/inventory-balances/:status, missingc.Bind) — all return 500. - Non-existent endpoints:
POST /deposits,POST /inventory/adjustments,GET /payments(list), andPOST /payments/searchreturn 404 — these endpoints are not implemented. To list/search payments, usePOST /cashflow-transactions/search(the unified transaction ledger — see Rule 63). - Attachments — full CRUD: Add:
POST /:type/:id/attachments(multipart,filefield,application/pdforimage/*— NOTtext/plain). List:GET /:type/:id/attachments. Delete:DELETE /:type/:id/attachments/:attachmentResourceId(HTTP 200). CLI:clio attachments add --file <path>or--url <url>,clio attachments list,clio attachments delete <attachmentResourceId>. Response shape is non-standard:{ reference, resourceId, attachments: [{fileName, fileType, fileId, attachmentResourceId}] }— NOT{ data: [...] }. The attachment ID field isattachmentResourceId(notresourceId). - Currency rate direction:
rate= functionalToSource (1 base = X foreign) — POSTrate: 0.74for a SGD org means 1 SGD = 0.74 USD. If your data stores rates as "1 USD = 1.35 SGD" (sourceToFunctional), you MUST invert:rate = 1 / 1.35 = 0.74. GET confirms both:rateFunctionalToSource(what you POSTed) andrateSourceToFunctional(the inverse).
Search & Filter
- Search endpoint universal pattern — All 28
POST /*/searchendpoints share identical structure:{ filter?, sort: { sortBy: ["field"], order: "ASC"|"DESC" }, limit: 1-1000, offset: 0-65536 }. Sort is REQUIRED when offset is present (evenoffset: 0). Default limit: 100.sortByis always an array on all endpoints (no exceptions). Seereferences/search-reference.mdfor per-endpoint filter/sort fields. 50a.queryfield — Jaz search operators — 14 endpoints accept an optionalquerystring alongsidefilter: invoices, bills, customer/supplier credit notes, journals, cashflow-transactions, bank-records, contacts, items, capsules, fixed-assets, scheduled-transactions, chart-of-accounts, tax-profiles. Example:{ "query": "status:unpaid AND $500+", "limit": 50 }. Key syntax: amounts ($500+,$100-500,amount:>2m, magnitude suffixes5k/2m/1b), negative ($-500), absolute value (abs:1000+), dates (date:this month,date:-30d,due:overdue,submitted:last week,lastpayment:-7d), status/enum (status:unpaid,currency:SGD,USD— comma = OR), string fields (customer:acme,ref:INV-*wildcard,=ref:INV-001exact,ref:/\d{4}/regex), blank checks (ref:blank,tag:!blank), booleans (hasattachment:yes,customer:yes), negation (!status:paidorNOT status:void— never-for negation), logic (AND/ORwith implicit AND on space), grouping, inline sort (sort:amount:desc). Full syntax spec (all fields, aliases, entity field lists, examples):references/search-syntax.md. 50b.query+filtermerge — When both are present, they are merged at the filter level. Explicitfilterkeys win on conflict. Usequeryfor human-readable shorthand,filterfor programmatic precision, or combine both:{ "query": "date:this year", "filter": { "currencyCode": { "in": ["SGD"] } } }. 50c.queryerror handling — Unknown field name →query_not_understood(400). Bad enum value (e.g.status:BADVALUE) → empty results, no error (silent miss). Unsupported endpoint →query_not_supported(400). Parser unavailable →query_parse_error(502). Empty/null/whitespace query → passthrough (ignored). In CLI/MCP: use--query/queryparam only on supported entities — unsupported entities have no--queryflag. - Filter operator reference — String:
eq,neq,contains,in(array, max 100),likeIn(array, max 100),reg(regex array, max 100),isNull(bool). Numeric:eq,gt,gte,lt,lte,in. Date (YYYY-MM-DD):eq,gt,gte,lt,lte,between(exactly 2 values). DateTime (RFC3339): same operators, converted to epoch ms internally. Boolean:eq. JSON:jsonIn,jsonNotIn. Logical: nest withand/or/notobjects, or useandGroup/orGrouparrays (invoices, bills, journals, credit notes). - Date format asymmetry (CRITICAL) — Request dates:
YYYY-MM-DDstrings (all create/update and DateExpression filters). Request datetimes: RFC3339 strings (DateTimeExpression filters forcreatedAt,updatedAt,approvedAt,submittedAt). ALL response dates:int64epoch milliseconds — includingvalueDate,createdAt,updatedAt,approvedAt,submittedAt,matchDate. Convert:new Date(epochMs).toISOString().slice(0,10). Timezone convention: ALL business dates (valueDate,dueDate,startDate,endDate, etc.) are in the organization's timezone — never UTC. The epoch ms stored in the DB represents the org-local date (no timezone conversion is ever needed). Only audit timestamps (createdAt,updatedAt,action_at) are UTC. - Field aliases on create endpoints — Middleware transparently maps:
issueDate/date→valueDate(invoices, bills, credit notes, journals).name→tagName(tags) orinternalName(items).paymentDate→valueDate,bankAccountResourceId→accountResourceId(payments).paymentAmount→refundAmount,paymentMethod→refundMethod(credit note refunds).accountType→classificationType,currencyCode→currency(CoA). Canonical names always work; aliases are convenience only. - All search/list responses are flat — every search and list endpoint returns
{ totalElements, totalPages, data: [...] }directly (no outerdatawrapper). Access the array viaresponse.data, pagination viaresponse.totalElements. Two exceptions: (a)GET /bank-accountsreturns a plain array[{...}](see Rule 18), (b)GET /invoices/:idreturns a flat object{...}(nodatawrapper) — unlikeGET /bills/:id,GET /contacts/:id,GET /journals/:idwhich wrap in{ data: {...} }. Normalize the invoice GET response before use. - Scheduled endpoints support date aliases —
txnDateAliasesmiddleware (mappingissueDate/date→valueDate) now applies to all scheduled create/update endpoints:POST/PUT /scheduled/invoices,POST/PUT /scheduled/bills,POST/PUT /scheduled/journals,POST/PUT /scheduled/subscriptions. - Kebab-case URL aliases —
capsuleTypesendpoints also accept kebab-case paths:/capsule-types(list, search, CRUD).moveTransactionCapsulesalso accepts/move-transaction-capsules. Both camelCase and kebab-case work identically.
Jaz Magic — Extraction & Autofill
- When the user starts from an attachment, always use Jaz Magic — if the input is a PDF, JPG, or any document image (invoice, bill, receipt), the correct path is
POST /magic/createBusinessTransactionFromAttachment. Do NOT manually construct aPOST /invoicesorPOST /billspayload from an attachment — Jaz Magic handles the entire extraction-and-autofill pipeline server-side: OCR, line item detection, contact matching, CoA auto-mapping via ML learning, and draft creation with all fields pre-filled. Only usePOST /invoicesorPOST /billswhen building transactions from structured data (JSON, CSV, database rows) where the fields are already known. - Two upload modes with different content types —
sourceType: "FILE"requires multipart/form-data withsourceFileblob (JSON body fails with 400 "sourceFile is a required field").sourceType: "URL"accepts application/json withsourceURLstring. The OAS only documents URL mode — FILE mode (the common case) is undocumented. - Three required fields + one optional:
sourceFile(multipart blob — NOTfile),businessTransactionType("INVOICE","BILL","CUSTOMER_CREDIT_NOTE", or"SUPPLIER_CREDIT_NOTE"—EXPENSErejected),sourceType("FILE"or"URL"). Optional:uploadMode("SEPARATE"default, or"MERGED"for a single PDF containing multiple documents — the backend splits it via boundary detection before extraction). All required fields are validated server-side. CRITICAL: multipart form field names are camelCase —businessTransactionType,sourceType,sourceFile,uploadMode, NOT snake_case. Usingbusiness_transaction_typereturns 422 "businessTransactionType is a required field". The File blob must include a filename and correct MIME type (e.g.application/pdf,image/jpeg) — bareapplication/octet-streamblobs are rejected with 400 "Invalid file type". 59a. MERGED upload workflow tracking — WhenuploadMode: "MERGED", the upload responseworkflowResourceIdis a parent tracking ID. The backend splits the PDF, then creates child workflows for each split page — these child IDs appear inPOST /magic/workflows/search(by fileName or createdAt), NOT the parent ID. To track MERGED progress, search byfileNamerather than the parentworkflowResourceId. - Response maps transaction types: Request
INVOICE→ responseSALE. RequestBILL→ responsePURCHASE. RequestCUSTOMER_CREDIT_NOTE→ responseSALE_CREDIT_NOTE. RequestSUPPLIER_CREDIT_NOTE→ responsePURCHASE_CREDIT_NOTE. S3 paths follow the response type. The responsevalidFiles[]array containsworkflowResourceIdfor tracking extraction progress viaPOST /magic/workflows/search. - Extraction is asynchronous — the API response is immediate (file upload confirmation only). The actual Magic pipeline — OCR, line item extraction, contact matching, CoA learning, and autofill — runs asynchronously. Use
POST /magic/workflows/searchwithfilter.resourceId.eq: "<workflowResourceId>"to check status (SUBMITTED → PROCESSING → COMPLETED/FAILED). When COMPLETED,businessTransactionDetails.businessTransactionResourceIdcontains the created draft BT ID. ThesubscriptionFBPathin the response is a Firebase Realtime Database path for real-time status updates (alternative to polling). - Accepts PDF and JPG/JPEG — both file types confirmed working. Handwritten documents are accepted at upload stage (extraction quality varies).
fileTypein response reflects actual format:"PDF","JPEG". - Never use magic-search endpoints —
GET /invoices/magic-searchandGET /bills/magic-searchrequire a separatex-magic-api-key(not available to agents). Always usePOST /invoices/searchorPOST /bills/searchwith standardx-jk-api-keyauth instead. 63b. Workflow search tracks all magic uploads —POST /magic/workflows/searchsearches across BT extractions AND bank statement imports. Filter byresourceId(eq),documentType(SALE, PURCHASE, SALE_CREDIT_NOTE, PURCHASE_CREDIT_NOTE, BANK_STATEMENT),status(SUBMITTED, PROCESSING, COMPLETED, FAILED),fileName(contains),fileType,createdAt(date range). Response: paginatedMagicWorkflowItemwithbusinessTransactionDetails.businessTransactionResourceId(the draft BT ID when COMPLETED) orbankStatementDetails(for bank imports). Standard search sort:{ sortBy: ["createdAt"], order: "DESC" }.
Cashflow & Unified Ledger
- No standalone payments list/search —
GET /payments,POST /payments/search, andGET /paymentsdo NOT exist. Per-payment CRUD (GET/PUT/DELETE /payments/:resourceId) exists for individual payment records, but to list or search payments, usePOST /cashflow-transactions/search— the unified transaction ledger that spans invoices, bills, credit notes, journals, cash entries, and payments. Filter bybusinessTransactionType(e.g.,SALE,PURCHASE) anddirection(PAYIN,PAYOUT). Response dates are epoch milliseconds. - Contacts search uses
name— NOTbillingName. The filter field for searching contacts by name isname(maps tobillingNameinternally). Sort field is alsoname. UsingbillingNamein a search filter returns zero results.
Response Shape Gotchas
- Contact boolean fields are
customer/supplier— NOTisCustomer/isSupplier. These are plain booleans on the contact object:{ "customer": true, "supplier": false }. UsingisCustomerorisSupplierin code will beundefined. - Finalized statuses differ by resource type — NOT
"FINALIZED","FINAL", or"POSTED". Journals →"APPROVED". Invoices/Bills →"UNPAID"(progresses to"PAID","OVERDUE"). Customer/Supplier Credit Notes →"UNAPPLIED"(progresses to"APPLIED"). All types support"DRAFT"and"VOIDED". When creating withoutsaveAsDraft: true, the response status matches the type's finalized status. - Create/pay responses are minimal — POST create endpoints (invoices, bills, journals, contacts, payments) return only
{ resourceId: "..." }(plus a few metadata fields). They do NOT return the full entity. To verify field values after creation, you MUST do a subsequentGET /:type/:resourceId. Never assert on field values from a create response. - No
amountDuefield — Invoices and bills do NOT have anamountDuefield. To check if a transaction is fully paid, inspect thepaymentRecordsarray: ifpaymentRecords.length > 0, payments exist. ComparetotalAmountwith the sum ofpaymentRecords[].transactionAmountto determine remaining balance. - Response dates include time component — Even though request dates are
YYYY-MM-DD, response dates are epoch milliseconds (see Rule 52). When comparing dates from responses, always convert withnew Date(epochMs).toISOString().slice(0, 10)— never string-match against the raw epoch value. Remember: business dates are org-timezone (see Rule 52). - Items POST requires
saleItemName/purchaseItemName— When creating items withappliesToSale: trueorappliesToPurchase: true, you MUST includesaleItemNameand/orpurchaseItemNamerespectively. These are the display names shown on sale/purchase documents. Omitting them causes 422: "saleItemName is a required field". If not specified, default to theinternalNamevalue. - Items PUT requires
itemCode+internalName— Even for partial updates,PUT /items/:idrequires bothitemCodeandinternalNamein the body. Omitting either causes 422. Use read-modify-write pattern: GET current item, merge your updates, PUT the full payload. Clio handles this automatically. - Capsules PUT requires
resourceId+capsuleTypeResourceId— Even for partial updates,PUT /capsules/:idrequiresresourceIdandcapsuleTypeResourceIdin the body. Omitting either causes 422 or "Capsule type not found". Use read-modify-write pattern: GET current capsule, merge updates, PUT full payload. Clio handles this automatically.
Cash Entry Response Shape (CRITICAL)
- Cash-in/out/transfer CREATE returns
parentEntityResourceId— The resourceId in the POST response ({ data: { resourceId: "X" } }) is the journal header'sparentEntityResourceId. This ID is used for DELETE (DELETE /cash-entries/X). But it is NOT the same ID used for GET (GET /cash-in-entries/:id). GET expects the cashflow-transactionresourceIdfrom the LIST response. Three different IDs exist per cash entry:parentEntityResourceId(from CREATE + in LIST),resourceId(cashflow-transaction ID, from LIST — use for GET),businessTransactionResourceId(underlying journal ID — do NOT use for anything). - Cash-in/out/transfer LIST/GET return cashflow-transaction shape — NOT journal shape. Key field differences from journals:
transactionReference(NOTreference),transactionStatus(NOTstatus— values:ACTIVE/VOID),valueDateis epoch ms (NOT ISO string), nojournalEntriesarray, hasdirection(PAYIN/PAYOUT), has nestedaccountobject with bank name, hasbusinessTransactionType(JOURNAL_DIRECT_CASH_IN/JOURNAL_DIRECT_CASH_OUT/JOURNAL_CASH_TRANSFER). - Cash-in/out/transfer search uses
/cashflow-transactions/search— Filter bybusinessTransactionType: { eq: "JOURNAL_DIRECT_CASH_IN" }(orJOURNAL_DIRECT_CASH_OUTorJOURNAL_CASH_TRANSFER). Other useful filters:organizationAccountResourceId(bank account),businessTransactionReference(reference),valueDate(date range). The search endpoint is shared across all cashflow transaction types. - DELETE for cash entries uses
/cash-entries/:id— NOT the individual resource paths. The ID used is theparentEntityResourceId(= the resourceId returned by CREATE). This is a shared endpoint for all cash entry types (cash-in, cash-out, cash-transfer).
Entity Resolution (Fuzzy Matching)
--contact,--account, and--bank-accountaccept names — any CLI flag that takes a contact, chart of accounts entry, or bank account accepts EITHER a UUID resourceId OR a fuzzy name. Examples:--contact "ACME Corp",--account "DBS Operating",--bank-account "Business". The CLI auto-resolves to the best match (strict thresholds) and shows the resolved entity on stderr. UUIDs are passed through without API calls. If the match is ambiguous, the CLI errors with a list of candidates — never silently picks the wrong entity.capsule-transactionrecipes auto-resolve accounts — when--inputis omitted, the CLI searches the org's chart of accounts for each blueprint account name (e.g., "Interest Expense", "Loan Payable"). If all accounts resolve with high confidence, no JSON mapping file is needed. If any fail, the error message shows exactly which accounts could not be found and suggests close matches.--contactand--bank-accounton recipes also accept names.- Payment/refund account filter is conditional on
--method— for BANK_TRANSFER, CASH, and CHEQUE, the--accountresolver filters to bank/cash accounts only. For other payment methods, all account types are considered.
Draft Finalization Pipeline (Convert & Next)
The clio bills draft subcommand group enables the full "review → fill missing → convert" workflow that mirrors the Jaz UI's "Convert and Next" button. Designed for AI agents processing a queue of draft bills.
Commands
| Command | Purpose |
|---|---|
clio bills draft list [--ids <ids>] [--json] |
Queue view: all drafts with per-field validation + attachment count |
clio bills draft finalize <id> [flags] [--json] |
Fill missing fields + convert DRAFT → UNPAID in one PUT |
clio bills draft attachments <id> [--json] |
List attachments with download URLs for agent inspection |
Mandatory Fields for Bill Finalization
| Field | JSON Path | CLI Flag | Resolver |
|---|---|---|---|
| Contact | contactResourceId |
--contact <name/UUID> |
Fuzzy resolved |
| Bill date | valueDate |
--date <YYYY-MM-DD> |
Literal |
| Due date | dueDate |
--due <YYYY-MM-DD> |
Literal |
| Line items | lineItems (non-empty) |
--lines <json> |
— |
| Item name | lineItems[i].name |
via --lines |
— |
| Item price | lineItems[i].unitPrice |
via --lines |
— |
| Item account | lineItems[i].accountResourceId |
--account <name/UUID> (bulk) |
Fuzzy resolved |
Optional: --ref, --notes, --tag, --tax-profile <name/UUID> (bulk, fuzzy resolved), --tax, --tax-inclusive, --dry-run, --input <file>.
Agent Workflow Pattern
Step 1: clio bills draft list --json
→ Batch queue: every DRAFT with per-field validation + attachment count
Step 2: For each draft where ready = false:
a) Read validation.missingFields from Step 1 output
b) Optional: clio bills draft attachments <id> --json
→ Download fileUrl, read PDF/image, extract or verify values
c) Resolve values (ask user, or infer from attachment + context)
d) clio bills draft finalize <id> --contact "Acme" --date 2025-01-15 ... --json
→ Updates + converts to UNPAID in one PUT (Rule 67: bills/invoices → UNPAID, journals → APPROVED)
Step 3: For each draft where ready = true:
clio bills draft finalize <id> --json
→ Converts directly (all mandatory fields already present)
--accountbulk patches line items — when used withclio bills draft finalize,--accountresolves the name to a UUID then setsaccountResourceIdon EVERY line item where it's currently null. Existing accounts are NOT overwritten. Same for--tax-profile.--linestakes priority (full replacement).--dry-runvalidates without modifying — returns the same validation structure asdraft list(per-field status/hint), so agents can preview what would happen before committing. No API write occurs.- Finalization is a single PUT —
updateBill()withsaveAsDraft: falsetransitions DRAFT → UNPAID (per Rule 67) and updates all fields in one call. No delete-and-recreate. The CLI handles all field normalization automatically (date format, line item sanitization, account field name mapping). - Draft list attachment count —
draft listincludesattachmentCountper draft (fromGET /bills/:id/attachments). Usedraft attachments <id>for full details includingfileUrldownload links. - PUT body requires
resourceId— The UpdateBill PUT endpoint requiresresourceIdin the body (in addition to the URL path). Dates must beYYYY-MM-DD(not ISO with time).taxInclusionis boolean (true/false), not string. Line items must useaccountResourceId(notorganizationAccountResourceIdfrom GET). - GET→PUT field asymmetry — GET returns
organizationAccountResourceIdon line items; PUT requiresaccountResourceId. GET returns dates as2026-02-27T00:00:00Z; PUT requires2026-02-27. GET returnstaxProfile: { resourceId }object; PUT requirestaxProfileResourceIdstring. The CLIdraft finalizecommand normalizes all of these automatically. - Magic workflow status may be null immediately after creation — The
POST /magic/workflows/searchendpoint may return a workflow withstatus: nullright afterPOST /magic/create-from-attachment. Allow 2-3 seconds before polling, or default toSUBMITTED. The CLImagic statuscommand defaults null status toSUBMITTED. - Finalized invoices/bills need
accountResourceIdon all line items — WhensaveAsDraft: false(or using--finalize), everylineItems[i].accountResourceIdmust be set. Omitting it causes 422: "lineItems[0].accountResourceId is required if [saveAsDraft] is false". The CLI validates this pre-flight.
DRY Extension Pattern
Bills, invoices, and credit notes share identical mandatory field specs. Adding clio invoices draft or clio customer-credit-notes draft later reuses all validation, formatting, and CLI flag logic from draft-helpers.ts — only the API calls differ.
Bank Rules
- Bank rules GET by ID has double-nested response —
GET /bank-rules/:idreturns{ data: { data: [...], totalElements, totalPages } }(doubledatawrapper). Unlike standardGET /:type/:idwhich returns{ data: {...} }. The innerdatais an array containing the single rule. Unwrap withresponse.data.data[0]. Field asymmetry: Request usesappliesToReconciliationAccount(string UUID), response returns it as an object{ code, currencyCode, name }. - Bank rules search uses
/bank-rules/search— Standard search pattern with filter/sort/limit/offset. Filter fields:appliesToReconciliationAccount,name,reference,resourceId,actionType,businessTransactionType. Sort fields:resourceId,name,actionType,businessTransactionType,reference,appliesToReconciliationAccount,createdAt. 90a. Bank rules create field isappliesToReconciliationAccount(NOTappliesToReconciliationAccountResourceId) — the bank account UUID.configurationmust nest underreconcileWithDirectCashEntrykey.configuration.reconcileWithDirectCashEntry.referenceis REQUIRED (omitting causes GENERAL_ERROR).amountAllocationType: use"PERCENTAGE"or"FIXED"—"FIXED_AND_PERCENTAGE"is read-only (include bothfixedAllocation+percentageAllocationarrays and the server infers it). Optional config fields:contactResourceId,
…(truncated)