Api
Complete agent stack for Jaz. Plugins, Skills, MCP tools, and CLI for Claude Code, Cowork, Codex, Copilot, Cursor, and more.
npx -y skills add teamtinvio/jaz-ai --skill apiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill whenever you call, debug, or review code that touches the Jaz REST API. Covers field names, response shapes, 158 production gotchas, error recovery (422/400/404/500), search filters, pagination, and edge cases for every endpoint — invoices, bills, credit notes, journals, cash entries, payments, contacts, CoA, items, tax profiles, bank records, fixed assets, schedulers, subscriptions, attachments, claim settings, and Jaz Magic extraction. Also use when building API clients, seeding test data, or adding new endpoint support.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
101.0 KB, as published. Nobody here has run it
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).
Pick the right invocation path first
Before touching this skill's HTTP details, check what's actually available:
- Running inside an MCP host (Claude Desktop, Cowork): use the MCP tools (
execute_toolwithcreate_invoice,list_bills, etc.). Do not write direct HTTP. The MCP server handles auth, retries, and field shape for you. - Running Claude Code with the
jaz-clioCLI: use the CLI commands (clio invoices list --json, etc.). Same code path, structured output. - No agent surface, raw integration: write HTTP calls per the endpoints catalog below.
The rest of this skill — field names, gotchas, error catalog, dependency order, search filter syntax — applies regardless of invocation path. Read it for context, not for HTTP-call construction unless you're in the third bucket.
Reading Order
Core fundamentals (read first, every integration): Identifiers & Dates 1–3; Names & Fields 9–13; Transaction Creation 14–16; Chart of Accounts 17–22; Payments / Cross-Currency 4–8; Journals & Cash 23–26; Credit Notes & Refunds 27–28; Reports 36–37; Tax Profile Scoping 100; Transaction References 104; Draft Finalization Pipeline 81–88; Jaz Magic / PDF-JPG 57–63; Currency Rates 39, 49, 105; Withholding Tax 45, 98.
Integration depth (API clients, pipelines, batch jobs, MCP/CLI): Bulk Upsert (Items/Contacts/Rates); Background Jobs (filter resourceId not jobId); Export Records; Pagination (38); Search & Filter (50–56); Response Shape Gotchas (66–73); Cash Entry Response Shape (74–77); Entity Resolution (78–80); Bank Rules (89–90c); Fixed Assets (91–92c); Subscriptions & Scheduled (93–94); niche endpoints (95–102); Journals balance (103); Quick Fix (107, 111); TTB (108); Dynamic Strings (109–110); Sub-Resource Shapes (112); Nano-Classifier (113); Scheduler Asymmetry (114); Payment Record CRUD (115–117); Bulk Upserts transactions (118–122); Reconciliation write-side (123–127); Drafts lifecycle (128–135); Orders — Sale Quotes / Sale Orders / Purchase Requests / Purchase Orders (references/orders.md); Claims — records / lifecycle / bulk + conversion + payouts + Employees + Claim Settings (references/claims.md).
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. Both fields accept the classic 12 types (Bank Accounts, Cash, Current Asset, Fixed Asset, Inventory, Current Liability, Non-current Liability, Shareholders Equity, Operating Revenue, Other Revenue, Operating Expense, Direct Costs) AND the IFRS 18 set added 2026-04 (Discontinued Expense, Discontinued Income, Finance Cost, Financing Income, Goodwill, Income Tax Expense, Investing Expense, Investing Income, Investment) — see rule 140 for IFRS 18 detail. - 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": 0.74 }(custom rate). Must be enabled for the org. Omit for base currency. Direction:exchangeRateis functionalToSource (1 org-base unit = NsourceCurrency) — usually the inverse of a quoted rate. Pass your figure as-is withrateDirectionrather than inverting by hand; Rule 49. 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, rateDirection?: "FUNCTIONAL_TO_SOURCE" | "SOURCE_TO_FUNCTIONAL" }. Direction:exchangeRateis functionalToSource (1 org-base unit = NsourceCurrency) — usually the inverse of a quoted rate. Pass your figure as-is withrateDirectionrather than inverting by hand; Rule 49. 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.offsetis a 0-indexed PAGE NUMBER, not a row-skip (offset=1 = second page oflimitrows). 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. Payload tier (view) — page-then-drill:search_*and the leanlist_*tools (invoices, bills, contacts, items, journals, customer/supplier credit notes, sale/purchase orders) return a compact summary row by default (view:"lean"— id + reference/status/date/contact/amount). Search lean to FIND a record, then read it in full via itsget_*; passview:"full"only when you need whole rows up front (heavier — avoid for broad searches). Other collections always return full. CLI defaults to full; use--view lean.
Other
- Currency rates use
/organization/currencies/:code/rates— 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). The older hyphenated/organization-currencies/...rate paths still resolve but are marked deprecated in the OpenAPI spec — prefer the nested form. 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: 0.74 }for a custom rate. Rate hierarchy: org rate → platform/ECB → transaction-level. Direction:exchangeRateis functionalToSource (1 org-base unit = NsourceCurrency) — usually the inverse of a quoted rate. Pass your figure as-is withrateDirectionrather than inverting by hand; Rule 49. - 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). You do not have to do the arithmetic:add_currency_rate,update_currency_rateand thecurrencyobject on the 8 FX create tools (invoice, bill, journal, both credit notes, both cash entries, TTB) accept an optionalrateDirection(FUNCTIONAL_TO_SOURCE|SOURCE_TO_FUNCTIONAL) — declare how your figure reads and pass it verbatim. Omitting it always meansFUNCTIONAL_TO_SOURCE. The two families differ only in where the label is applied: the rate-table tools (plusbulk_upsert_currency_rates) send it on the wire and the server applies it, while the 8 FX create tools have no such field, so the client inverts and strips it there. Prefer either to inverting by hand: a wrong inversion is silent and wrong by rate².clio calc fx-revalREQUIRES the direction — that calculator historically assumed the opposite convention to the API.
Search & Filter
- Search endpoint universal pattern — All 32
POST /*/searchendpoints share identical structure:{ filter?, sort: { sortBy: ["field"], order: "ASC"|"DESC" }, limit: 1-1000, offset: 0-65536 }.offsetis a 0-indexed page number (not row-skip). 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. If you hold the invoice/bill as raw HTML (e.g. an email body), pass it viasourceType: "HTML"with anhtmlfield — the backend renders it to a PDF then extracts, so you do NOT need to save it to a file first. - Three 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.sourceType: "HTML"accepts the raw HTML body in anhtmlfield (JSON or multipart) — the backend renders it to a PDF, then extracts (use this for an email body you already hold; no file needed). The OAS only documents URL mode — FILE and HTML modes are undocumented there. - Three required fields + one optional: the source (
sourceFilemultipart blob — NOTfile— for FILE,sourceURLfor URL, orhtmlfor HTML),businessTransactionType("INVOICE","BILL","CUSTOMER_CREDIT_NOTE", or"SUPPLIER_CREDIT_NOTE"—EXPENSErejected),sourceType("FILE","URL", or"HTML"). For HTML mode,htmlis the raw HTML string (max 5 MB). 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". - 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 by default — 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, do a subsequentGET /:type/:resourceId. MCP tool shortcut:create_invoice/create_bill/create_journal/create_contact/create_itemacceptreturnFullEntity: true— the executor performs the GET server-side and returns the full entity inline, saving a turn. The raw RESTPOSTis still minimal-only; only the MCP tools collapse the round trip. If the post-create GET fails (transient 5xx, network blip), the tool returns the minimal create envelope augmented with_hydration: { status: 'failed', resourceId, message }— the write committed; the agent should retry only theget_*call, NEVER the create (would duplicate the document). - 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,internalNotes,tags,currencySettings,taxCurrencySettings,classifierConfigon allocation lines. 90b. Bank rules PUT is FULL REPLACEMENT —PUT /bank-rules/:idreplaces the entire rule. Must sendresourceId,appliesToReconciliationAccount, and fullconfigurationevery time. Omitting any required field causes GENERAL_ERROR. Use read-modify-write pattern: GET current rule, merge changes, PUT full payload. Same pattern as items PUT (Rule 72) and capsules PUT (Rule 73). 90c. Bank rule POST canonical create payload (verified live 2026-04, NOT obvious from any single field error):
{
"name": "My Rule",
"appliesToReconciliationAccount": "<bank-account-uuid>",
"configuration": {
"reconcileWithDirectCashEntry": {
"amountAllocationType": "PERCENTAGE",
"reference": "AUTO-{{bankReference}}",
"percentageAllocation": [
{ "organizationAccountResourceId": "<acct-uuid>", "amount": 100 }
]
}
}
}
Required nested keys (each missing one returns a different cryptic error):
configurationMUST nest underreconcileWithDirectCashEntrykey (the action type — even though there's only one type today)amountAllocationType:"PERCENTAGE"or"FIXED"only (NOTFULL_AMOUNTdespite that sounding right)- For
PERCENTAGE:percentageAllocation: [{organizationAccountResourceId, amount}](ARRAY of allocations summing to 100) - For
FIXED:fixedAllocation: [{organizationAccountResourceId, amount}](ARRAY of fixed amounts) referenceis required in the action config (omitting → cryptic 422)
Dynamic strings: name and reference support {{bankReference}}, {{bankPayee}}, {{bankDescription}} placeholders — replaced with bank record values during reconciliation.
actionShortcutResourceId for apply_bank_rule IS the rule's own resourceId from create response (no separate "action shortcut" entity).
90d. Bank rules support column-value mapping — reconcileWithDirectCashEntry can resolve fields per row from a custom bank-statement column: amountSourceColumnKey (set amount by a column; mutually exclusive with amount), organizationAccountResourceIdMap/taxProfileResourceIdMap/classifierConfigMap on each fixedAllocation line, and contactResourceIdMap/tagsMap on the shortcut. Each *Map is a ColumnValueMapConfig (columnKey + ordered mappings[], first match wins, blank matchValue = catch-all); targetResourceId is a UUID (tag NAME for tagsMap). reference also takes {{column:<key>}} / {{column_abs:<key>}} tokens. Full shape: references/bank-rule-column-mapping.md.
Fixed Assets
- Fixed asset search does NOT support
createdAtsort — Valid sort fields:resourceId,name,purchaseDate,typeName,purchaseAmount,bookValueNetBookValueAmount,depreciationMethod,status. UsingcreatedAtreturns 422. Default topurchaseDateDESC. - Fixed asset disposal/sale/transfer use different endpoint patterns — Discard:
POST /discard-fixed-assets/:id(body includesresourceId+ dates). Mark sold:POST /mark-as-sold/fixed-assets(body-only, no path param). Transfer:POST /transfer-fixed-assets(body-only). Undo:POST /undo-disposal/fixed-assets/:id. 92a. Two ways to register fixed assets — (1) Create (POST /fixed-assets): for assets purchased via a bill or journal already in the system. ACTIVE assets requirepurchaseBusinessTransactionType(PURCHASEorJOURNAL_MANUAL) andpurchaseBusinessTransactionResourceId. (2) Transfer (POST /transfer-fixed-assets): for pre-existing assets purchased before using Jaz or outside the system. AcceptsbookValueAccumulatedDepreciationAmountfor depreciation already incurred. No linked transaction needed. 92b.saveAsDraftdefaults totrue— To create an ACTIVE fixed asset, passsaveAsDraft: falsewith ALL required fields:name,category,typeCode,purchaseAmount,purchaseDate,purchaseAssetAccountResourceId,depreciationMethod,effectiveLife, and forSTRAIGHT_LINE:depreciationStartDate,accumulatedDepreciationAccountResourceId,depreciationExpenseAccountResourceId. Omitting any returns 422. 92c. Valid enums —depreciationMethod:STRAIGHT_LINE,NO_DEPRECIATION.category:TANGIBLE,INTANGIBLE. Optional string fields (purchaseBusinessTransactionResourceId,accumulatedDepreciationAccountResourceId,capsuleResourceId) can be safely omitted — the API ignores empty values.
Subscriptions & Scheduled Transactions
- Subscription endpoints are under
/scheduled/subscriptions— List, GET, POST, PUT, DELETE all at/api/v1/scheduled/subscriptions[/:id]. Cancel is PUT (not POST) at/api/v1/scheduled/cancel-subscriptions/:id(different path pattern). Subscriptions are invoices only (SALE) — no bills. Different from scheduled invoices: subscriptions auto-prorate partial periods (generate credit notes for mid-period changes), but currency/tax/account are immutable after creation. Use scheduled invoices for fixed-amount recurring invoices where you need per-occurrence flexibility. All subscription CRUD requiresproratedConfig: { proratedAdjustmentLineText: string }— Clio auto-injects this; do not add manually.repeatis required on POST (valid:ONE_TIME,DAILY,WEEKLY,MONTHLY,YEARLY) — Clio maps from theintervalparameter. Cancel requirescancelDateType(END_OF_CURRENT_PERIOD,END_OF_LAST_PERIOD,CUSTOM_DATE) +proratedAdjustmentLineText+resourceIdin body. Must cancel before delete.businessTransactionTypeis NOT in the OAS — the API ignores it. - Scheduled transaction search does NOT support
createdAtsort —POST /scheduled-transaction/searchsort fields:startDate,nextScheduleDate, etc. Default tostartDateDESC. This is a cross-entity search across all scheduled types (invoices, bills, journals, subscriptions). Filter bybusinessTransactionType(SALE, PURCHASE, JOURNAL) and/orschedulerType(RECURRING, SUBSCRIPTION) to narrow results.
Contact Groups
- Contact groups have
associatedContactsarray — Each group contains{ name, resourceId, associatedContacts: [{ name, resourceId }] }. Search viaPOST /contact-groups/search. Known bug: PUT returns 500 (Rule 46).
Inventory
- Inventory balance uses
GET /inventory-item-balance/:itemResourceId— Returns{ itemResourceId, latestAverageCostAmount, baseQty, baseUnit }. Note: this is the ITEM resourceId, not an inventory-specific ID. The/inventory-balances/:statusendpoint returns 500 (Rule 46).
Withholding Tax
- Withholding tax codes via
GET /withholding-tax-codes— Returns a flat array of 1,360+ entries (PH PSIC codes). Each entry:{ code, description, taxRate, ... }. No pagination — full list in one call. Use for PH/SG tax compliance. - Duplicate detection fields — API rejects duplicates with 422: Contacts on
name(NOTbillingName), Items onitemCode, Accounts onname, Tax Profiles onname. Agent tools auto-search before creating — if a match is found, the existing entity is returned instead of hitting a 422.
Tax Profile Scoping
- Tax profiles have
appliesToSale/appliesToPurchasescope — A sales-only tax profile used on a bill causes 422. Always filter by transaction type when selecting:search_tax_profilesacceptsappliesToparam (sale,purchase,sale_credit_note,purchase_credit_note). Invoices →sale, Bills →purchase.
Cash Entry PUT Requirements
- Cash entry PUT requires
accountResourceId+resourceId—PUT /cash-in-entries/:idandPUT /cash-out-entries/:idrequire bothaccountResourceId(bank account) andresourceIdin the body. OmittingaccountResourceIdcauses 500.accountEntryResourceIdis optional (auto-populated).
Cash Entry Account Type
- Cash-in/out
accountResourceIdmust be a Bank Accounts type — Using expense, revenue, or other non-bank accounts causes 422CASH_OUT_ACCOUNT_TYPE_NOT_ALLOWED(or equivalent for cash-in). Uselist_bank_accountsorsearch_accountswithaccountType: "Bank Accounts"to find valid accounts.
Journals
- Journal entries must balance — Sum of all DEBIT amounts must exactly equal sum of all CREDIT amounts. Unbalanced journals are rejected with 422. Agent tools pre-flight check this client-side before hitting the API.
Transaction References
- Invoice/bill/CN references must be unique per org — Creating a transaction with a
referencethat already exists causes 422Sale Reference already exists(orPurchase Reference). Generate unique references with timestamps (e.g.,INV-20260309-1430) when the user doesn't specify one.
Currency Rates
add_currency_ratefor new rates,update_currency_rateonly for editing existing records — When a user says "update the rate" or "set the rate", useadd_currency_rate(POST — creates a new rate entry for a date). Only useupdate_currency_rate(PUT) when explicitly modifying an existing rate record by its resourceId.- Contact PUT uses
email(string), notemails(array) — GET returnsemails: [{email, label}](array) but PUT acceptsemail: "[email protected]"(string). Sending theemailsarray in PUT body causes 400 "Invalid request body". The CLI and tool executor handle this automatically via read-modify-write with the correct field.
Quick Fix (Bulk Update)
- 20 Quick Fix endpoints for bulk-updating transactions and line items —
POST /api/v1/quick-fix/{entity}with{ resourceIds: [...], attributes: {...} }. Only included fields are changed — omitted fields are left unchanged. Response:{ updated: string[], failed: [{ resourceId, error, errorCode }] }. HTTP status codes: 200 = complete success (failedalways empty). 207 Multi-Status = partial or total failure with per-item detail (same response shape as 200 — checkfailedarray). 422 = total failure with no per-item breakdown (rare). On 207, retry onlyfailedresourceIds. Entities: ARAP: invoices, bills, customer-credit-notes, supplier-credit-notes. Accounting: journals, cash-entries. Schedulers: sale-schedules, purchase-schedules, subscription-schedules, journal-schedules. Line-item request patterns: ARAP + accounting use{ lineItemResourceIds, attributes }. Schedulers (sale/purchase/subscription) use Pattern C:{ schedulerUpdates: [{ schedulerResourceId, lineItemUpdates: [{ arrayIndex, ...attrs }] }] }. Journal-schedules use Pattern D:lineItemResourceId(UUID) instead ofarrayIndex. Field gotchas: cash entries usecurrencySetting(singular:{ rateFunctionalToSource, exchangeToken }), NOTcurrencySettings. Journal schedules havestartDatein addition toendDate/interval. Tags: string array, max 50 items, max 50 chars each.
Transfer Trial Balance
- Transfer Trial Balance (
POST /api/v1/transfer-trial-balance) creates opening balance entries. UsesjournalEntries(NOTlines— this is a journal type). Always ACTIVE (no draft mode), reference auto-generated as "Transfer Trial Balance", minimum 1 entry, entries cannot have 0 amounts, skips lock date validation.valueDatemust be today or in the past — future dates are rejected with "Opening data cannot be future date". Each entry:{ accountResourceId, type: "DEBIT"|"CREDIT", amount }.
Scheduler Dynamic Strings
- Scheduler placeholder strings — All scheduled transactions (invoices, bills, journals, subscriptions) support dynamic strings in any free text field (
reference, line itemname,notes). Strings are replaced with values relative to the transaction date:{{Day}}→ day name (Monday),{{Date}}→ full date (09 Mar 2026),{{Date+X}}→ date + X days,{{DateRange:X}}→ date range spanning X days (min 1, max 999),{{Month}}→ month name (March),{{Month+X}}→ month + X months,{{MonthRange:X}}→ month range spanning X months,{{Year}}→ year (2026),{{Year+X}}→ year + X years. Example:reference: "INV-{{Month}}-{{Year}}"→"INV-March-2026"for a March transaction.
Bank Rule Dynamic Strings
- Bank rule placeholder strings — Bank reconciliation rules support dynamic strings in any free text field (
name,reference, cash entry description). Strings are replaced with actual bank record values during reconciliation:{{bankReference}}→ bank record reference (e.g., INV-03/01/2025-01),{{bankPayee}}→ payer/payee name (e.g., Fruit Planet),{{bankDescription}}→ transaction description (e.g., QR Payment). Example:reference: "{{bankPayee}} - {{bankReference}}".
Quick Fix Tag Field
- Quick-fix uses
tags(string array) — e.g.,"tags": ["Q1"]— Theattributesobject in quick-fix transaction endpoints acceptstagsas a string array (e.g.,"tags": ["Q1"]).tag(singular) is silently ignored. This matches thetagsarray format used on create/update for all transaction types. The CLI--tagflag auto-wraps totags: [name].
Sub-Resource Response Shapes
- Invoice/bill payment & credit sub-resources return raw arrays —
GET /invoices/:id/paymentsandGET /bills/:id/paymentsreturn[{paymentRecord}, ...]— NOT{data: [...]}. Same forGET /invoices/:id/creditsandGET /bills/:id/credits. The CLI wraps these into{data: [...]}for consistency.DELETE /invoices/:id/credits/:creditsAppliedResourceIdreverses a credit application.
Nano-Classifier API
- Nano-classifier API gotchas — CREATE uses
classes: string[](NOTclassNamesor[{className}]).printable: booleanis required — defaults tofalse(most classifiers are not printable). GET single is double-wrapped:{data: {data: [...], totalElements, totalPages}}— extract the first element from the inner paginated response. GET/LIST response returns classes as[{className, resourceId}](objects), while CREATE accepts plainstring[].
Scheduler Response Asymmetry
- Scheduler response uses
interval, notrepeat— POST/PUT usesrepeatfield (values:ONE_TIME,DAILY,WEEKLY,MONTHLY,YEARLY;QUARTERLYis rejected with 422). GET response returnsintervalfield (same values; legacy rows may still showQUARTERLY). PUT accepts the full transaction template (invoice,bill, or journal entries at top level), not just schedule metadata — same structure as POST.
Payment Record CRUD
-
Payment record CRUD —
GET /payments/:resourceIdreturns{data: PaymentRecord}(wrapped). Payment resourceIds come from invoice/bill GET response →paymentRecords[].resourceId. Cashflow transaction IDs ≠ payment IDs — don't mix them.POST /cashflow-transactions/searchreturns cashflow IDs, while payment CRUD uses separate payment IDs from the parent document. -
PaymentMethod accepts 11 values — All payment endpoints (invoices, bills, credit note refunds, payment updates) accept:
CASH,BANK_TRANSFER,CREDIT_CARD,CHEQUE,E_WALLET,WITHHOLDING_TAX_CERTIFICATE,CLEARING_SETTLEMENT,DEBT_WRITE_OFF,INTER_COMPANY,OTHER,PAYMENT_GATEWAY. Default isBANK_TRANSFER. The OAS previously listed only 7 — the API runtime already accepted all 11. -
Fixed asset sale accepts PURCHASE type —
saleBusinessTransactionTypein mark-as-sold acceptsSALE,PURCHASE, orJOURNAL_MANUAL. UsePURCHASEwhen the disposal is linked to a purchase-side transaction (e.g., trade-in).
Bulk Upserts (transactions)
-
8 bulk-upsert endpoints for transactions —
POST /api/v1/{invoices,bills,customer-credit-notes,supplier-credit-notes,journals,fixed-assets}/bulk-upsertplus line-item variants for invoices and bills (/invoices/line-items/bulk-upsert,/bills/line-items/bulk-upsert). Max 500 rows per call. All async — return{data: {jobId, subscriptionFBPath, status, totalRecords}}. Pollsearch_background_jobswithfilter: {resourceId: {eq: jobId}}until terminal status. Natural keys: invoices =invoiceReference, bills =billReference, credit notes =creditNoteReference, journals =journalReference(NOTreference— asymmetric vs other entities), fixed assets =reference.currencyCodeis REQUIRED on every transaction row (invoices, bills, CCN, SCN) — missing it returns errorCodeIMPORT_CURRENCY_REQUIRED. Journals are the exception: the bulk journal row has no currency field and anycurrencyCodesent is discarded, and the public request model exposes no per-leg currency field either. Journals legs usejournalEntries[](NOTentries[]— different fromjournals createwhich uses entries), and a journal leg isorganizationAccountResourceId+ exactly one ofdebitAmount/creditAmount— NOT theaccountResourceId+amount+typeshapejournals createtakes, and omit the unused side rather than sending 0. ProvideresourceId(UUID) to update by ID; otherwise the natural key drives upsert (journals upsert byjournalReferenceonly).rowIndexis optional caller-supplied for error reporting — on journals it sits on the leg, not the row. -
PARTIAL_SUCCESS handling — When
search_background_jobsreturnsPARTIAL_SUCCESSfor a bulk-upsert job, the per-row failures are indata[0].errorDetailson the SAME response (an array of per-row error objects). Top-level counts (processedCount,failedCount,totalRecords) tell you how many failed;errorDetailstells you which rows and why. Don't pretend the operation succeeded — surface the failed rows to the user. The rule of thumb: poll withsearch_background_jobsfiltered byresourceId: { eq: jobId }, then readdata[0].errorDetailsfor terminal states. -
dateFormatfield was removed from bulk-upsert — the API now requires ISO 8601 (YYYY-MM-DD) for ALL date fields onPOST /{invoices,bills,journals}/bulk-upsert. SendingdateFormat: "MM/DD/YYYY"(or any other value) is silently ignored. Reject any datetime strings (anything withTor:) client-side before submitting. -
Intra-batch reference dedup is MERGE, not REJECT — Two rows in the same bulk-upsert call sharing the same natural key (
invoiceReference/billReference/etc.) are MERGED by the API (last row wins). If the user wants strict-uniqueness, dedup client-side first. -
Fixed asset bulk upsert — date fields are inconsistent — the bulk-upsert REQUEST has TWO different date conventions:
valueDateisYYYY-MM-DD(string);depreciationStartDateis epoch milliseconds (number). Sending YYYY-MM-DD fordepreciationStartDatereturns generic 400 "Invalid request body" with no detail. The GET RESPONSE usespurchaseDate(different field from requestvalueDate). SendingpurchaseDatein the request → same generic 400. Other fields:costandpurchaseAmountare synonyms;effectiveLifeandusefulLifeMonthsare synonyms. Required:reference,registrationType("NEW" | "TRANSFER"). Recommended:typeCode(e.g.FURNITURE_AND_FIXTURE),typeName,category("TANGIBLE" | "INTANGIBLE"),cost/purchaseAmount,valueDate(YYYY-MM-DD),effectiveLife/usefulLifeMonths,depreciationMethod(STRAIGHT_LINE|NO_DEPRECIATION),purchaseAssetAccountResourceId(UUIDv4),depreciationExpenseAccountResourceId(UUIDv4),accumulatedDepreciationAccountResourceId. To setdepreciationStartDate: pass epoch ms OR omit (defaults to valueDate).
Reconciliation actions (write-side)
-
12 reconciliation action endpoints under
/api/v1/reconciliations/*— these commit a reconciliation decision against a bank statement entry, distinct fromview_auto_reconciliation(which queries/search-magic-reconciliationfor suggestions): - Async (jobId):quick_reconcile(bulk match entries to journals, max 500),apply_bank_rule(bulk apply a rule to entries, max 500). Pollsearch_background_jobsfiltered byresourceId; onPARTIAL_SUCCESSreaddata[0].errorDetailsfor per-row failures. - Sync (single bank entry):reconcile_direct_cash_entry,reconcile_cash_journal,reconcile_manual_journal,reconcile_cash_transfer,reconcile_invoice_receipt,reconcile_bill_receipt,reconcile_with_payments(match EXISTING — see Rule 158),reconcile_learned_prediction. Each returns{bankStatementEntryResourceId, status, reference, valueDate}. - Sync bulk:reconcile_magic_match(bulk-accept MAGIC_MATCH suggestions, max 500) returns{reconciled[], failed[]};undo_reconciliations(unlink, 1-500) returns{resetReconciliationResponse[], linkedRecords[]}— see Rule 125. -
Recon prefill from the bank statement entry — when caller omits
valueDate,dueDate, paymentamount, or direction (cash-in vs cash-out), the API fills these from the bank entry. Best-effort: a missing entry lookup logs a warning and forwards the payload as-is. Caller can always override by passing the field explicitly. -
The 6 sync recon endpoints are NOT idempotent — calling twice on the same
bankStatementEntryResourceIdcreates duplicate journals. Before retrying, confirm the entry's reconciled state viaview_auto_reconciliationorsearch_bank_recordsfiltered bystatus. Concurrent calls on the same entry race — last-write-wins.undo_reconciliationsis the reverse and behaves differently: it UNLINKS but does NOT delete the record that was matched, so undoing any of the 6 CREATE endpoints (direct_cash_entry,cash_journal,manual_journal,cash_transfer,invoice_receipt,bill_receipt) leaves that record on the books against an unmatched bank line — a discrepancy the undo introduced. Delete it vialinkedRecords[](captured BEFORE the unlink, since the unlink is what makes it unfindable by bank entry): cash in/out/transfer byparentEntityResourceId, everything else bybusinessTransactionResourceId. Never just re-reconcile after undoing — that creates a SECOND record. Undo never double-applies (a repeat returns per-entryFAILED/INVALID_BANK_STATEMENT_ENTRY_STATUS), and unresolved ids are OMITTED from the response, so compare returned ids against what you sent. -
Sync recon → AR/AP via
invoice_receipt/bill_receipt— these endpoints CREATE a transaction (invoice for AR, bill for AP) and immediately reconcile it to the bank entry. The two endpoints stay separate (not unified) because the invoice side carriesbillTo/billFromthat bills don't have. Cash-in vs cash-out, by contrast, IS unified intoreconcile_direct_cash_entry— direction is encoded in the bank entry sign. -
Cash transfer
amountis conditional — forreconcile_cash_transfer,amountis required only when the counterparty account is in a non-functional currency. Same-currency transfers omit it; the API derives the amount from the bank entry.
Drafts lifecycle (server-side, BULK-friendly)
-
3 BULK-action server-side draft lifecycle endpoints under
/api/v1/drafts/*— distinct from the local-onlysrc/core/drafts/payload helpers. All three accept a single mixed-type batch (max 500 items per call); there are NO per-entity variants. ONE call covers any combination of invoices, bills, customer credit notes, and supplier credit notes. -POST /api/v1/drafts/validate(sync) →validate_drafts. Returns per-item validation errors + display data inline. No state change. Use to pre-flight before convert/submit. -POST /api/v1/drafts/convert-to-active(async, jobId) →convert_drafts_to_active. Promotes drafts to ACTIVE. Pollsearch_background_jobsfiltered byresourceId; on PARTIAL_SUCCESS readdata[0].errorDetails. -POST /api/v1/drafts/submit-for-approval(async, jobId) →submit_drafts_for_approval. Routes drafts into the approval workflow. -
Drafts lifecycle request shape (mix-friendly) — all 3 endpoints accept the same body:
{ items: [{btResourceId, btType}] }withbtType ∈ {SALE | PURCHASE | SALE_CREDIT_NOTE | PURCHASE_CREDIT_NOTE}. Max 500 items per call. ONE batch can mix any combination of types — no need to group by btType client-side, no need to make multiple calls per entity type. Mapping:SALE→ invoice,PURCHASE→ bill,SALE_CREDIT_NOTE→ customer credit note,PURCHASE_CREDIT_NOTE→ supplier credit note. Journals are NOT in the enum: they have no approval state (ACTIVE | VOID | DRAFT), so they never entered this lifecycle. Promote a DRAFT journal withbulk_update_journals(saveAsDraft: false) orupdate_journal— that IS the supported path, not a workaround. -
Drafts lifecycle is NOT idempotent — a second
convert_drafts_to_activeon already-ACTIVE drafts returns 422; a secondsubmit_drafts_for_approvalon drafts with an in-flight approval returns 422. Filter the draft list bystatus: DRAFTbefore submitting (the entity-specific search tools —search_invoices,search_bills, etc. — acceptstatusfilters).validate_draftsIS safe to call repeatedly (read-only, no state change). -
Drafts must be COMPLETE before convert/submit —
convert_drafts_to_activeandsubmit_drafts_for_approvalreject INCOMPLETE drafts (drafts missing required fields likeaccountResourceIdon every line item, contactResourceId, etc.) with 422. Thebills draft list/invoices draft listetc. commands return per-draftready: bool+missingCount+missingFields[]— callvalidate_draftsfirst to surface missing fields per row, fix client-side, then convert/submit. -
Manual-journal recon: caller provides ONLY the offset side(s) —
reconcile_manual_journalAUTO-ADDS the bank-side leg from the bank statement entry. If the caller sends both debit AND credit legs covering the bank side, the API doubles-up after auto-add → 422 "sum of debit and credit amounts are not equal in a journal". Send only the offset journal entries (the non-bank side); the API balances them against the bank entry. -
reconcile_invoice_receiptandreconcile_bill_receiptgate onpaymentDirection, not BSE entry type — error codeINVALID_BUSINESS_TRANSACTION_TYPE_ERROR("Invalid business transaction type", 422) is misleadingly named. The actual gate is the BSE'spaymentDirection.invoice_receiptrequiresPAYIN(AR — money in — positive amount inbank add-records, producingcredit_amount > 0).bill_receiptrequiresPAYOUT(AP — money out — NEGATIVE amount, producingdebit_amount > 0).bank add-recordsproduces both directions based on amount sign. Statement-imported BSEs (bank import) also work — direction is set from the CSV. No need for the async magic-OCRbank importpath; syncbank add-recordswith the correct sign is sufficient. -
Bulk-upsert FLAT vs NESTED variants — for invoices and bills, there are TWO bulk-upsert endpoints. FLAT (
bulk_upsert_invoices/bulk_upsert_bills) — one line per row, set at row level viaitemDescription+totalAmount+invoiceAccountResourceId(orbillAccountResourceId). NESTED (bulk_upsert_invoice_line_items/bulk_upsert_bill_line_items) — multi-line per row via nestedlineItems[], each withitemDescription+quantity+unitPrice+accountResourceId. Use FLAT when one line per transaction is fine (CSV import, simple bills); use NESTED when each transaction needs multiple lines. SendinglineItems[]to the FLAT endpoint silently ignores them and creates a $0 invoice. -
Reconciliation
lineItems[]use a DIFFERENT field naming convention — forreconcile_invoice_receipt.invoiceDetails.lineItems[]andreconcile_bill_receipt.billDetails.lineItems[], each line usesname(NOTitemDescription) for the description andorganizationAccountResourceId(NOTaccountResourceId) for the revenue/expense account. The bulk-upsert-line-items variants useitemDescription+accountResourceId. Memorize this: bulk =itemDescription+accountResourceId; recon-create =name+organizationAccountResourceId. -
Sync bulk-upsert response carries per-row failures —
bulk_upsert_currency_ratesandbulk_upsert_chart_of_accountsreturn{ resourceIds: string[], failedRows: ImportedRowError[], failedCount: number }synchronously (no jobId polling needed). EachfailedRowsentry:{ rowIndex, columnName, columnValue, errorCode, errorMessage }. EmptyfailedRows: []+failedCount: 0on full success. Forbulk_upsert_currency_ratesspecifically: omittingrateApplicableTodefaults it torateApplicableFrom - 0.999ms(prevents temporal gaps in rate lookups). Contrast with async bulk-upserts (contacts, invoices, journals, etc.) which return{ jobId }and needsearch_background_jobspolling — there, per-row failures live in the job'serrorDetailsfield instead. -
bulk_upsert_contactsrequest-level validation — fails the WHOLE batch with HTTP 422 (no per-row partial success at this layer). Five rules to satisfy before submitting: (a) every contact must havecustomer: trueORsupplier: trueafter defaults+backfill — for updates, the API backfills omitted flags from the existing contact; for creates, you must explicitly set at least one. (b)emailList[]entries within a contact must be case-insensitively unique. (c)customerPaymentTerms.valueandsupplierPaymentTerms.valuemust be positive integers whenname!= "CUSTOM". (d)namemust be unique within the batch (after whitespace+case normalize). (e) WhenbillingAddressorshippingAddressis provided, itsaddressLine1is required. Pre-validate client-side before calling; one bad row rejects the entire batch and the agent loses any successful work-in-progress. -
get_contact_signals— read-only contact-history pattern lookup —GET /api/v1/contacts/{resourceId}/signals?btType=…returns the contact's modal patterns (currency, payment-terms, tax-inclusion/presence, top-COA, top-item), cadence (median interval days, days-since-last, interval ratio), and outstanding-balance snapshot for one (contact × business-transaction-type) pair.btTypeis required:SALE|PURCHASE|SALE_CREDIT_NOTE|PURCHASE_CREDIT_NOTE. Returns nulldatawhen sampleSize < 5 or the freshness layer is unavailable. Cache key is per-(contactId, btType) pair, so repeated calls for the same pair are cheap. Three slices are always empty/null on this endpoint —severitySummary,outlierFlags,revealedDivergences— because they require a draft to compare against; those populate only on the per-resultcontactSignalsobject insidevalidate_draftsresponses. Use this tool for "what does this contact normally look like?" questions before drafting; usevalidate_draftsfor "how does this draft compare to the contact's history?" questions after drafting. -
validate_draftsper-result enrichment (MID7) — every entry inresults[]now carries two extra slices alongside the existingeligible/errors[]/displayData[]: (a)contactSignals— full Mid-7 insight (cadence, outliers, severity, divergences, outstanding balance) computed against the draft's contact history. Null when the draft has no contact, the draft is ineligible, or the contact has no qualifying history in the 12-month window. Same shape asget_contact_signalsbut populated WITH the always-empty-on-GET slices (severitySummary, outlierFlags, revealedDivergences) — those compare the draft against the contact's modal pattern. (b)breakdown— full Balance-panel payload (items[]+metawith subtotal / tax / total / paymentRecorded / balance / exchangeRate). Use breakdown to surface the trx-level metadata an agent needs for "show me what this draft looks like" questions without a separateget_invoice/get_billcall. Top-level:eligibleCount,ineligibleCount,columns/errorColumns(table render hints), andcontactSignalsMeta.unavailable=truewhen the freshness layer was offline for the whole batch (per-resultcontactSignalswill all be null). Wire response uses legacy field namescontactInsight(per-result) andcontactInsightsMeta(top-level); motherboard's API client renames both tocontactSignals/contactSignalsMetafor consistency withget_contact_signals. -
IFRS 18 accountType values (effective 2027) —
create_account/update_account/bulk_upsert_chart_of_accountsaccept the 9 IFRS 18 classification types alongside the classic 12: Discontinued Expense, Discontinued Income, Finance Cost, Financing Income, Goodwill, Income Tax Expense, Investing Expense, Investing Income, Investment.normalizeAccountType(incore/api/guards.ts) maps unambiguous variants client-side: "income tax" / "tax expense" → "Income Tax Expense", "finance costs" → "Finance Cost", "investments" → "Investment". Ambiguous variants are intentionally NOT auto-mapped — under IFRS 18, "interest expense" can land in EITHER Finance Cost (financing activities) OR Operating / Investing Expense depending on the entity's main business activity, and "interest income" can land in EITHER Financing Income OR Investing Income. The agent must pick the explicit canonical string for those cases instead of relying on a guess that could misclassify the account. Pass any value toaccountType(POST/PUT will receive it asclassificationTypeper rule 21). The classic types still work — IFRS 18 is purely additive. -
bulk_upsert_chart_of_accounts— sync bulk-upsert with PARTIAL_SUCCESS — wrapsPOST /api/v1/chart-of-accounts/bulk-upsert(max 500 per call). Returns synchronously (no jobId polling):{ resourceIds: string[], failedRows: ImportedRowError[], failedCount: number }per rule 136. Each successful row contributes oneresourceId; each failure surfaces afailedRows[]entry withrowIndex(1-based per the API),columnName,columnValue,errorCode,errorMessage. Dedup is by NAME, not code — collisions emitORGANIZATION_CHART_OF_ACCOUNT_DUPLICATEDper row (other rows in the batch still succeed). ProvideresourceIdper account to update; omit to create. Accepts the classic 12 + 9 IFRS 18accountTypevalues per rule 140 (variants normalized client-side). For one-off creates with auto-dedup-on-name (returns existing if found), usecreate_accountinstead. CLI counterpart:clio accounts bulk-upsert --input <file.json>. -
capsuleRecipepayload is mutually exclusive withcapsuleResourceIdon trigger mutations (create/update of invoice, bill, journal, cash_in, cash_out). UsecapsuleRecipeto CREATE a new capsule via the recipe engine; usecapsuleResourceIdto ATTACH a base-trx to an existing capsule. Sending both returns 422 (excluded_withvalidator). -
Capsule recipe publish is best-effort post-commit — silent-null failure mode. On success the trigger-mutation response carries
capsuleRecipeJob: { jobResourceId, capsuleResourceId, subscriptionFBPath, totalRecords, idempotentHit, recipeKey }(verified live 2026-05-27). NotejobResourceId(NOTresourceId) on the trigger-mutation payload — this is the polling key. On publish failure,capsuleRecipeJobis absent (or null) from the response, the trigger mutation STILL returns 201, the base-trx is committed, and NO error reason is surfaced to the caller. The response echoescapsuleRecipe.{recipeName, inputs}back unchanged, which can look like success at a glance. Three known causes of silent nullcapsuleRecipeJob(must pre-validate before sending):
- (a) Wrong
recipeNamefor the base trx type — every recipe is locked toallowedBaseTransactionTypes(e.g. PREPAID_AMORTIZATION = PURCHASE only; DEFERRED_REVENUE = SALE only; ACCRUAL_REVERSAL, IFRS16_LEASE = JOURNAL_MANUAL only; LOAN_AMORTIZATION = JOURNAL_DIRECT_CASH_IN or JOURNAL_MANUAL). Onpreview_capsule_recipe, mismatch surfaces as 422RECIPE_INVALID_BASE_TRANSACTION_TYPE. On the trigger mutation, it silently nulls the job — the validation happens post-commit in customer-service and arap catches the exception. Always checkget_capsule_recipe(name).allowedBaseTransactionTypesmatches the trigger mutation you're calling. - (b) Currency mismatch — see Rule 156 (single-currency v1 recipes — recipe
currency, every*AccountResourceIdaccount'scurrencyCode, and the base trx currency MUST all match). Mismatch surfaces as 422ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCHonpreview_capsule_recipebut silently nulls on the trigger mutation. - (c) Wrong
x-accountClasson an input field — see Rule 157 (each*AccountResourceIdslot has a required account class). Mismatch silently nulls; preview returns the matchingRECIPE_FIELDS_*422.
Diagnosis sequence when the response has null/absent capsuleRecipeJob:
- Re-run
preview_capsule_recipewith the samerecipeName+inputs(without a base trx). This is the canonical pre-flight: it returns the exact 422 reason —ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH,RECIPE_INVALID_BASE_TRANSACTION_TYPE,RECIPE_FIELDS_MUST_DIFFER, etc. Most reliable diagnostic — fix the input and retry the trigger. - Poll
search_background_jobs --filter '{"baseTransactionResourceId":{"eq":"<id>"}}'— if aFAILEDjob exists, itserrorDetailshas the publish failure reason. If no job exists for the base-trx, the publish never queued (validation rejected pre-queue in customer-service). resume_capsule_recipe(capsuleResourceId)is only available if a capsule WAS created — i.e. the recipe partially ran. For pre-queue rejections (3 causes above), no capsule exists; the only recovery is to re-issue the trigger mutation with corrected inputs.
Pre-flight gate (recommended for agents and integrations): always call preview_capsule_recipe(recipeName, inputs) before the trigger mutation. Preview is pure-compute (no side effects) and surfaces every input/account/currency problem with a clear error_type — eliminates the silent-null class entirely. Same gate covers templateOverrides (Customize Recipe): pass capsuleRecipe.templateOverrides: [{slotKey, template}] to customize generated text (capsule title/description, leg labels, line memos, schedule reference). Valid slotKeys + {{variables}} come from get_capsule_recipe → versions[].templateSlots[]; slotKey ≤128 chars (no dups), template ≤2000 (empty string clears a nullable slot). Invalid overrides return 422 ERR_RECIPE_OVERRIDE_* (UNKNOWN_RECIPE / MISSING_SLOT_KEY / DUPLICATE_SLOT / UNKNOWN_SLOT / NON_NULLABLE_BLANK / TEMPLATE_TOO_LONG / UNKNOWN_VARIABLE) on preview but silently null on the trigger path — so preview first.
-
recipeNameIS enum-constrained at the API layer (verified live 2026-05-27): closed enumLOAN_AMORTIZATION | ACCRUAL_REVERSAL | PREPAID_AMORTIZATION | DEFERRED_REVENUE | IFRS16_LEASEonPOST /capsule-recipes/previewand oncapsuleRecipe.recipeNamepayloads on trigger mutations. Send a string not in the set → 422 validation_error. Don't hard-code the 5 values in motherboard descriptions — discover vialist_capsule_recipes(the source of truth) and pass the discovered name through. -
Pseudo-SQL
truncated:truedoes NOT mean "you hit the cap" — it means "more rows matched than were returned in this preview". InspectrowCountvs preview cap (100) or your LIMIT to interpret. If you need every row, switch toexport_pseudo_sql. -
Pseudo-SQL export
downloadUrlis S3 pre-signed with ~15min expiry (X-Amz-Expires=900). Fetch immediately; don't store the URL. If a fetch returns 403 (expired), callget_pseudo_sql_export(jobId)again for a fresh URL. -
Cashflow report (
download_export(exportType='cashflow')) returns the org's CASHFLOW template (IAS 7). If no template configured, returns 404template_not_found. Configure via Jaz settings before invoking. -
resume_capsule_recipeafterterminalReason=BLOCKED_AFTER_3_RESUME_ATTEMPTSis unavailable. Only path forward isrollback_capsule_recipe(capsuleResourceId)or manual cleanup via Jaz admin. Resume is NOT idempotent — each call counts toward the 3-attempt limit. -
rollback_capsule_recipereturningstatus=PARTIAL_ROLLBACKwithblockedAtomResourceIds[]is safe to retry (rollback is idempotent on already-deleted atoms). Persistent partial-rollback typically indicates an atom is referenced downstream; escalate to ops if retry doesn't resolve. -
preview_capsule_recipereturns 422RECIPE_INVALID_BASE_TRANSACTION_TYPEwhen the recipe'sallowedBaseTransactionTypesdoesn't include the supplied base trx type (see descriptor atget_capsule_recipe). The trigger mutation does NOT surface this 422 to the caller — it silently nullscapsuleRecipeJobon the response (see Rule 143). The allowed types are: PREPAID_AMORTIZATION → PURCHASE only; DEFERRED_REVENUE → SALE only; ACCRUAL_REVERSAL → JOURNAL_MANUAL only; IFRS16_LEASE → JOURNAL_MANUAL only; LOAN_AMORTIZATION → JOURNAL_DIRECT_CASH_IN or JOURNAL_MANUAL. Always checkget_capsule_recipe(name).allowedBaseTransactionTypesbefore sendingcapsuleRecipeon any trigger mutation. -
Sending BOTH
capsuleRecipeANDcapsuleResourceIdon the same trigger mutation returns 422 (excluded_withvalidator — same lock as Rule 142). Pick one based on intent. -
saveAsDraft: true+capsuleRecipepayload — recipe is stashed in the draft'spending_capsule_recipeJSONB column and fires on draft activation (not on draft create). The base-trx commits as DRAFT immediately; the recipe job is created later when the draft is activated viaconvert_drafts_to_active. -
Pseudo-SQL
Idempotency-Keydedup is server-side primary key — same key + DIFFERENT query body returns the prior job's result (the server does NOT cross-check the new query body). For agent reliability,run_pseudo_sql_and_downloadauto-keys fromsha256(query).slice(0,16)so dedup is query-tied automatically. If you callexport_pseudo_sqldirectly with a manual key, treat it as a per-intent token — don't reuse across different queries. -
rollback_capsule_recipeon a non-recipe capsule (a capsule created by the legacycreate_capsuletool or imported, not by the recipe engine) returns 422RECIPE_ROLLBACK_JOB_NOT_FOUND("No CAPSULE_RECIPE job found for capsule X in organization Y — nothing to roll back"). Rollback only works on capsules whose lifecycle was managed by the recipe engine. For legacy capsules, usedelete_capsuleinstead. -
Pseudo-SQL schema is canonical — call
get_pseudo_sql_schemabefore any query. The response returns the live curated catalog (tables / columns / joins / functions) PLUS the canonicaljaz-pseudo-sql.mdskill body inagentSkillsDoc.content. Drop the.mdbody into context as the syntax-rules source; treattables[] / joins[] / functions[]as the column-list source. Cache contract: theversionfield is a stable 16-char hex hash; within a session, cache by version and don't re-call unless you have reason to believe the schema changed (e.g. a fresh backend deploy mid-session). Don't re-fetch on a wall-clock timer (upstream isprivate, no-cache, must-revalidate). A static curated-schema snapshot used to ship with thejaz-pseudo-sqlskill before v5.6.0; it was dropped because it was structurally guaranteed to drift. Never write a query from a memorized column list. -
v1 capsule recipes are single-currency —
ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH. The recipecurrencyfield, every*AccountResourceIdaccount'scurrencyCode, and the base transaction'scurrencyCodeALL MUST match. Preview returns 422ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCHwith a concrete message (e.g. "account X is denominated in USD but the recipe currency is SGD"). Trigger mutation silently nullscapsuleRecipeJob(Rule 143). Practical recipe: never hardcodecurrency; derive fromget_account(prepaidAssetAccountResourceId).currencyCode(or the base trx currency) and use the same value as the recipe input. The recipe descriptor'scurrency.x-baseTrxBinding: "strict"field marks this — when present, the value is bound to (and must equal)trx.currencyCodepost-commit. Caught by smoke runs since v5.5.0 — tests 65/66 hardcodedSGDand silently nulled against a USD fire-test org for 20+ hours. -
Recipe input
*AccountResourceIdfields are account-class-locked —x-accountClassin inputSchema is authoritative. Each*AccountResourceIdslot on a recipe's input schema carries anx-accountClassconstraint ("Asset","Liability","Expense","Revenue","Equity"). Passing an account whose class doesn't match the slot'sx-accountClassis rejected post-commit and silently nullscapsuleRecipeJob(Rule 143). Schema location:get_capsule_recipe(name).data.versions[0].inputSchema.properties.<fieldName>['x-accountClass']— noteversions[0].inputSchema, NOTinputSchemaat the top level. Examples: PREPAID_AMORTIZATION needsprepaidAssetAccountResourceId: Asset+expenseAccountResourceId: Expense; DEFERRED_REVENUE needsdeferredRevenueAccountResourceId: Liability+revenueAccountResourceId: Revenue; ACCRUAL_REVERSAL needsexpenseAccountResourceId: Expense+accruedLiabilityAccountResourceId: Liability. Always pre-validate viaget_account(resourceId).accountClassagainst the slot constraint, or just callpreview_capsule_recipe(recipeName, inputs)to surface every class violation as a clean 422.
Supporting Files
For detailed reference, read these files in this skill directory:
- references/search-syntax.md — Full Jaz search query syntax: amounts, dates, abs, blanks, wildcards, regex, entity field lists, aliases, examples (auto-synced from dashboard repo)
- references/search-reference.md — API search/filter/sort reference for all 28 endpoints — per-endpoint filter fields, sort fields, operator types
- references/endpoints.md — Full API endpoint reference with request/response examples
- references/errors.md — Complete error catalog: every error, cause, and fix
- references/field-map.md — Complete field name mapping (what you'd guess vs actual), date format matrix, middleware aliases
- references/dependencies.md — Resource creation dependencies and required order
- references/full-api-surface.md — Complete endpoint catalog (80+ endpoints), enums, search filters, limits
- references/feature-glossary.md — Business context per feature — what each feature does and why, extracted from help.jaz.ai
- help-center-mirror/ — Full help center content split by section (auto-generated from help.jaz.ai)
Help Center Knowledge Base (clio help-center / clio hc)
For product questions (how-to, feature behavior, troubleshooting), use clio help-center instead of reading raw help-center-mirror files:
clio help-center "how to apply credit note" # search help center
clio help-center "bank recon" --limit 3 # limit results
clio help-center "scheduled invoices" --section invoices # filter by section
Supports --json for structured output. 186 articles across 20 sections. Automatically uses hybrid search (embeddings + keyword) when available, falls back to keyword + synonym expansion offline.
When to use clio help-center vs reading raw files:
- Use
clio help-centerwhen you need specific answers (returns only relevant articles, saves context) - Read
help-center-mirror/*.mddirectly only when you need to scan an entire section comprehensively
Dashboard Deep Links (navigation tools)
When the user wants to OPEN, SEE, or SHARE something in the Jaz dashboard ("open this invoice", "take me to the P&L"), hand them a real URL via ONE agent (MCP) tool — deliberately no CLI command: navigate. Called with no destination it discovers destination keys (filter by query/resource/kind; keys are named by product area, not task wording — on zero matches retry broader: "billing", not "card"); called with a destination it turns the key into { url, kind, label }. Screens are dotted route paths (reports.profit-and-loss); record modals are <resource>.modal.<type> (sales.modal.view-sale). Record-specific modals (view/edit/duplicate one record) REQUIRE resourceId — the same Jaz resourceId from the search/get that found the record; screens and create/"new" modals take none (a resourceId on a screen is rejected). NEVER write a dashboard URL by hand and NEVER guess a destination key — routes are not guessable, and a wrong link is worse than no link; if navigate errors, follow its hint (near-matches + discovery pointer) and say the link could not be built rather than improvising one. The org query param is appended automatically when the surface knows the acting org; API-key surfaces open in the user's current org on the app.jaz.ai host. Examples: "open invoice INV-042" → search_invoices → navigate("sales.modal.view-sale", resourceId); "take me to the P&L" → navigate("reports.profit-and-loss"); "where do I manage users?" → navigate(query: "user") → navigate("settings.modal.user_management"). (The pre-merge tool names find_dashboard_destinations / get_dashboard_url still resolve as aliases.)
Recommended Client Patterns
- Starting from an attachment? → Use Jaz Magic (
POST /magic/createBusinessTransactionFromAttachment). Never manually parse a PDF/JPG to constructPOST /invoicesorPOST /bills— let the extraction & autofill pipeline handle it. - Starting from structured data? → Use
POST /invoicesorPOST /billsdirectly with the known field values. - Serialization (Python):
model_dump(mode="json", by_alias=True, exclude_unset=True, exclude_none=True) - Field names: All request bodies use camelCase
- Date serialization: Python
datetype →YYYY-MM-DDstrings - Bill payments: Embed in bill creation body (safest). Standalone
POST /bills/{id}/paymentsalso works. - Bank records: Create via JSON
POST /bank-records/:idor multipartPOST /magic/importBankStatementFromAttachment. Search viaPOST /bank-records/:id/searchwith filters (valueDate, status, description, extContactName, netAmount, extReference). - Scheduled invoices/bills: Wrap as
{ status, startDate, endDate, repeat, invoice/bill: { reference, valueDate, dueDate, contactResourceId, lineItems, saveAsDraft: false } }.referenceis required. - Scheduled journals: Flat:
{ status, startDate, endDate, repeat, valueDate, schedulerEntries, reference }.valueDateis required. - FX currency (invoices, bills, credit notes, AND journals):
currency: { sourceCurrency: "USD" }(auto-fetches platform rate) orcurrency: { sourceCurrency: "USD", exchangeRate: 0.74 }(custom rate). Same object form on all transaction types. Never usecurrencyCodestring — silently ignored.
-
Match-to-EXISTING reconciliation —
reconcile_with_payments/reconcile_magic_match/reconcile_learned_prediction. These reconcile a bank entry against transactions/payments the org ALREADY has, vsinvoice_receipt/bill_receiptwhich CREATE new ones. Prefer match-to-existing to avoid duplicates. -reconcile_with_payments— the headline.businessTransactionPayments[]each carry an open bill/invoice'scashflowTransactionResourceId(fromsearch_cashflow_transactionsor a suggestion'scftBtResourceId) +transactionAmount; the endpoint CREATES the payment AND reconciles in one call — nopay_bill/pay_invoicefirst. Also acceptsmatchedPayments[](existing payments) /matchedBatchPayments[]/adjustment(over/under-payment + FX write-off). Guard: ≥1 match array non-empty. FX is auto-resolved server-side — pass NOcurrencySettings/rate for the common case. Only the rare bill-currency ≠ bank-currency case needs explicitpaymentAmount(bank ccy) +currencySettings. FX gain/loss is NOT auto-posted — post it viaadjustment.cashAdjustmentEntries[]to an FX account. Errors:PAYMENT_AMOUNT_REQUIRED_IN_BUSINESS_TRANSACTION_SOURCE_CURRENCY(cross-ccy missing paymentAmount),INVALID_EXCHANGE_RATE_ERROR(adjustment leg in non-functional ccy missing rate),TOTAL_RECONCILIATION_AMOUNT_MISMATCHED_WITH_STATEMENT_ENTRY_AMOUNT(sum ≠ entry → add adjustment leg). NOT idempotent, no client key — a blind retry double-creates a payment; re-checksearch_bank_records(status:'RECONCILED')before retry. -reconcile_magic_match— bulk-accept MAGIC_MATCH suggestions (max 500 entries). Returns{reconciled[], failed[]}— a 200 with non-emptyfailed[]is a PARTIAL success (per-entryerrorCode); loop on failed only. Entry-level idempotency-keyed server-side (re-submit returns done entries inreconciled[]). -reconcile_learned_prediction— accept an ML prediction.predictedPayload+predictedPayloadSchemaVersioncome VERBATIM from aview_auto_reconciliation(MAGIC_RECONCILE_WITH_CASH_IN_OUT) suggestion — never hand-construct.retryTokenforces a fresh journal on edit-retry; omit for idempotent replay. On failure (stale payload), fall back toreconcile_with_payments— don't retry the blob. -
view_auto_reconciliationreturns execution-readysuggestions[]— the suggestion→commit seam.bankStatementEntryResourceIdsis REQUIRED — the endpoint is per-entry and has no account-wide mode; source ids fromsearch_bank_records(statusUNRECONCILED). Each suggestion carriesrecommendedTool(the commit tool to call),execute(ready-to-pass args — mergebankAccountResourceIdforreconcile_magic_match),confidenceTier(high/medium/low, code-derived), andautoCommitEligible(true ⇒ high confidence + executable plan + under anyautoCommitMaxAmountcap). Decision gate:autoCommitEligible===true→ auto-commit viarecommendedTool+execute; everything else → surface for confirmation. Amount threshold is a HARD VETO over confidence (passautoCommitMaxAmount). Field mapping under the hood:cftBtResourceId→cashflowTransactionResourceId(single →reconcile_with_payments),cftBtResourceIds[]/isBatch→matchedBusinessTransactions(batch →reconcile_magic_match),recommendationType→tool,confidenceScore→tier. Request vs response vocabulary: therecommendationTypeyou SEND selects what to compute and additionally acceptsRECOMMENDATIONS(every type exceptMAGIC_MATCH, ranked together, and the cheaper call —MAGIC_MATCHscans open transactions so it is requested on its own; nothing returns everything at once). TherecommendationTypeyou RECEIVE on a suggestion is always a concrete type and is neverRECOMMENDATIONS, so therecommendationType→tool mapping above is unaffected by which selector you sent. PassincludeRaw:truefor the unmapped payload. Cost tracks the entries you pass — batch large backlogs. On 500 it returns{degraded:true}— use theclio jobs bank-recon matchcascade. NOT idempotent applies to every commit — see Rules 125 + 158.
See Also
- jaz-recipes — 16 IFRS-compliant transaction recipes with journal entries, capsules, and calculators
- jaz-jobs — 12 accounting job playbooks (month-end close, bank recon, GST/VAT filing, etc.)
- jaz-conversion — Data migration workflows from Xero, QuickBooks, Sage, MYOB, and Excel
- jaz-cli — CLI command reference, auth, output formats, pagination, and workflow patterns