jaz-cli 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1299 @@
1
+ # Jaz API Endpoint Reference
2
+
3
+ > Full request/response examples for every Jaz API endpoint.
4
+ > See SKILL.md for rules, errors.md for troubleshooting, field-map.md for name lookups.
5
+
6
+ ---
7
+
8
+ ## Base URL & Auth
9
+
10
+ ```
11
+ Base URL: https://api.getjaz.com
12
+ Auth Header: x-jk-api-key: <key>
13
+ Content-Type: application/json
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Date Format (All Endpoints)
19
+
20
+ **All dates must be `YYYY-MM-DD` strings** (e.g., `"2026-02-08"`).
21
+
22
+ - ISO datetime strings (e.g., `"2026-02-08T00:00:00Z"`) are REJECTED — bill payment validation returns "does not match 2006-01-02 format"
23
+ - Epoch milliseconds are REJECTED
24
+ - The OAS may declare some date fields as `integer/int64` (e.g., cash journals) but `YYYY-MM-DD` strings work in practice
25
+ - production clients sends all dates as `YYYY-MM-DD` via Python `date` type
26
+
27
+ ---
28
+
29
+ ## Pagination (All List Endpoints)
30
+
31
+ All GET list endpoints and POST `/search` endpoints use **`limit`/`offset` pagination** — NOT `page`/`size`.
32
+
33
+ | Property | Value |
34
+ |----------|-------|
35
+ | **GET list endpoints** | `?limit=100&offset=0` (query params) |
36
+ | **POST /search endpoints** | `{ "limit": 100, "offset": 0 }` (JSON body) |
37
+ | **Default limit** | 100 (if omitted) |
38
+ | **Default offset** | 0 (if omitted) |
39
+ | **Max limit** | 1000 |
40
+ | **Min limit** | 1 |
41
+ | **Max offset** | 65536 |
42
+ | **Response shape** | `{ totalPages, totalElements, data: [...] }` |
43
+
44
+ **`page`/`size` are NOT supported** — sending `?page=0&size=100` is silently ignored (API returns default 100 items as if no params were sent). Always use `limit`/`offset`.
45
+
46
+ **POST /search sort requirement**: When `offset` is present in the body (even `offset: 0`), `sort` is required:
47
+ ```json
48
+ { "limit": 100, "offset": 0, "sort": { "sortBy": ["createdAt"], "order": "DESC" } }
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 1. Organization
54
+
55
+ ### GET /api/v1/organization
56
+
57
+ ```json
58
+ // Response (returns a LIST, not single object):
59
+ {
60
+ "totalElements": 1,
61
+ "totalPages": 1,
62
+ "data": [{
63
+ "resourceId": "31eb050a-...",
64
+ "name": "Jaz Global SG",
65
+ "currency": "SGD",
66
+ "countryCode": "SG",
67
+ "status": "ACTIVE",
68
+ "lockDate": null
69
+ }]
70
+ }
71
+ ```
72
+
73
+ Access org via `data[0]`. Check `lockDate` — don't seed transactions before it.
74
+
75
+ ---
76
+
77
+ ## 2. Chart of Accounts
78
+
79
+ ### GET /api/v1/chart-of-accounts?limit=200&offset=0
80
+
81
+ ```json
82
+ // Response (flat list, NOT double-nested):
83
+ {
84
+ "totalElements": 52,
85
+ "totalPages": 1,
86
+ "data": [{
87
+ "resourceId": "uuid",
88
+ "name": "Business Bank Account",
89
+ "status": "ACTIVE",
90
+ "accountClass": "Asset",
91
+ "accountType": "Bank Accounts",
92
+ "code": "90",
93
+ "currencyCode": "SGD",
94
+ "locked": false,
95
+ "controlFlag": true
96
+ }]
97
+ }
98
+ ```
99
+
100
+ ### POST /api/v1/chart-of-accounts/bulk-upsert
101
+
102
+ ```json
103
+ // Request:
104
+ {
105
+ "accounts": [
106
+ {
107
+ "name": "Sales Revenue",
108
+ "currency": "SGD",
109
+ "classificationType": "Operating Revenue",
110
+ "code": "4000"
111
+ },
112
+ {
113
+ "name": "Office Expenses",
114
+ "currency": "SGD",
115
+ "classificationType": "Operating Expense",
116
+ "code": "5000"
117
+ }
118
+ ]
119
+ }
120
+
121
+ // Response:
122
+ { "data": { "resourceIds": ["uuid1", "uuid2"] } }
123
+ ```
124
+
125
+ Upsert matches by name — existing accounts updated, new ones created.
126
+
127
+ **Important**: The bulk-upsert endpoint does NOT return individual resourceIds for created/updated accounts. After a successful bulk-upsert, you MUST re-fetch the full CoA via `GET /api/v1/chart-of-accounts` to collect the new resourceIds.
128
+
129
+ **CRITICAL: CoA code mapping — match by NAME, not code**:
130
+ - Pre-existing accounts may have different codes than your templates
131
+ - Example: "Cost of Goods Sold" = code 310 in the API, but code 5000 in template
132
+ - "Accounts Receivable" can have `code: null` in the API
133
+ - Always map template accounts to resource IDs via **name matching**, not code matching
134
+ - Resource IDs are the universal identifier, not codes
135
+ - When building lookup maps, key by BOTH `name` AND `code` for maximum compatibility:
136
+ ```javascript
137
+ ctx.coaIds[acct.name] = acct.resourceId;
138
+ if (acct.code) ctx.coaIds[acct.code] = acct.resourceId;
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 3. Tax Profiles
144
+
145
+ ### GET /api/v1/tax-profiles?limit=100&offset=0
146
+
147
+ ```json
148
+ // Response:
149
+ {
150
+ "totalElements": 10,
151
+ "totalPages": 1,
152
+ "data": [{
153
+ "resourceId": "d8a5afbb-...",
154
+ "taxTypeCode": "STANDARD_RATED_SUPPLIES",
155
+ "displayName": "Standard-Rated Supplies (SR)",
156
+ "vatValue": 9,
157
+ "status": "ACTIVE"
158
+ }]
159
+ }
160
+ ```
161
+
162
+ READ-ONLY. Map `taxTypeCode` to `resourceId`. Never create.
163
+
164
+ SG defaults: SR (9%), TX (9%), OS (0%), ZR (0%), ES (0%), EP (0%), IM (9%), plus a few more.
165
+
166
+ ---
167
+
168
+ ## 4. Currencies
169
+
170
+ ### GET /api/v1/organization/currencies
171
+
172
+ ```json
173
+ // Response (flat):
174
+ {
175
+ "totalElements": 1,
176
+ "totalPages": 1,
177
+ "data": [{
178
+ "currencyCode": "SGD",
179
+ "currencyName": "Singapore Dollar",
180
+ "currencySymbol": "S$",
181
+ "baseCurrency": true
182
+ }]
183
+ }
184
+ ```
185
+
186
+ ### POST /api/v1/organization/currencies
187
+
188
+ ```json
189
+ // Request:
190
+ { "currencies": ["USD", "EUR", "GBP"] }
191
+
192
+ // Response:
193
+ { "data": { "resourceIds": ["uuid1", "uuid2", "uuid3"] } }
194
+ ```
195
+
196
+ Enable currencies first, then set rates via the **separate** rate endpoints below.
197
+
198
+ ### Currency Rates — `/organization-currencies` (hyphenated path)
199
+
200
+ **CRITICAL path difference**: Enable uses `/organization/currencies` (nested). Rates use `/organization-currencies` (hyphenated). Using the wrong path returns 404.
201
+
202
+ #### POST /api/v1/organization-currencies/:currencyCode/rates
203
+
204
+ ```json
205
+ // Request:
206
+ { "rate": 0.74, "rateApplicableFrom": "2026-02-10" }
207
+
208
+ // Response:
209
+ { "data": "Rate added successfully" }
210
+ // HTTP 201
211
+ ```
212
+
213
+ **Required fields**:
214
+ - `rate` — positive number (must be > 0). Direction is **functionalToSource** (1 base = X foreign). Example for SGD org setting USD rate: `rate: 0.74` means 1 SGD = 0.74 USD. **If your data is sourceToFunctional (1 USD = 1.35 SGD), invert: `rate = 1 / yourRate`.**
215
+ - `rateApplicableFrom` — `YYYY-MM-DD` string (NOT ISO datetime — `"2026-02-10T00:00:00Z"` is rejected with "does not match 2006-01-02 format")
216
+
217
+ **Optional fields**:
218
+ - `rateApplicableTo` — `YYYY-MM-DD` string. Must be after `rateApplicableFrom` (422 `INVALID_DATE_RANGE` otherwise).
219
+
220
+ #### GET /api/v1/organization-currencies/:currencyCode/rates
221
+
222
+ ```json
223
+ // Response:
224
+ {
225
+ "data": {
226
+ "totalElements": 12,
227
+ "totalPages": 1,
228
+ "data": [{
229
+ "resourceId": "uuid",
230
+ "rateFunctionalToSource": 0.74,
231
+ "rateSourceToFunctional": 1.3514,
232
+ "rateApplicableFrom": "2026-01-01",
233
+ "rateApplicableTo": "2026-01-31",
234
+ "sourceCurrencyCode": "USD",
235
+ "functionalCurrencyCode": "SGD",
236
+ "notes": { "date": "2026-01-01", "name": "Admin" }
237
+ }]
238
+ }
239
+ }
240
+ ```
241
+
242
+ #### PUT /api/v1/organization-currencies/:currencyCode/rates/:resourceId
243
+
244
+ ```json
245
+ // Request:
246
+ { "rate": 0.71, "rateApplicableFrom": "2026-02-10" }
247
+
248
+ // Response:
249
+ { "data": "Rate updated successfully" }
250
+ // HTTP 200
251
+ ```
252
+
253
+ #### DELETE /api/v1/organization-currencies/:currencyCode/rates/:resourceId
254
+
255
+ ```json
256
+ // Response:
257
+ { "data": { "message": "Rate deleted successfully" } }
258
+ // HTTP 200
259
+ ```
260
+
261
+ **Boundary behavior**:
262
+ - Base currency rates → 400: `"Cannot set rate for organization base currency"` (GET also 400: `"Cannot lookup rate for organization base currency"`)
263
+ - Invalid ISO code (e.g., `"XYZ"`) → 422: `"validation_error"` (vs 404 for valid-but-not-enabled codes)
264
+ - `rate: 0` or negative → 422: `"rate must be greater than 0"`
265
+ - Very small rates (e.g., `0.0001`) are accepted
266
+
267
+ ---
268
+
269
+ ## 5. Contacts
270
+
271
+ ### GET /api/v1/contacts?limit=100&offset=0
272
+
273
+ ```json
274
+ // Response item:
275
+ {
276
+ "resourceId": "uuid",
277
+ "name": "Sterling Enterprises",
278
+ "billingName": "Sterling Enterprises",
279
+ "customer": true,
280
+ "supplier": false,
281
+ "currency": "SGD",
282
+ "phone": "+6591234567",
283
+ "email": "accounts@sterling.sg",
284
+ "taxNumber": "201812345A"
285
+ }
286
+ ```
287
+
288
+ ### POST /api/v1/contacts
289
+
290
+ ```json
291
+ // Request:
292
+ {
293
+ "name": "Sterling Enterprises",
294
+ "billingName": "Sterling Enterprises",
295
+ "customer": true,
296
+ "supplier": false,
297
+ "currency": "SGD",
298
+ "phone": "+6591234567",
299
+ "email": "accounts@sterling.sg",
300
+ "taxNumber": "201812345A",
301
+ "addressLine1": "100 Robinson Road",
302
+ "addressLine2": "#08-01",
303
+ "city": "Singapore",
304
+ "postalCode": "068902",
305
+ "countryCode": "SG"
306
+ }
307
+
308
+ // Response:
309
+ { "data": { "resourceId": "uuid", ... } }
310
+ ```
311
+
312
+ ---
313
+
314
+ ## 6. Items
315
+
316
+ ### GET /api/v1/items?limit=100&offset=0
317
+
318
+ ```json
319
+ // Response item (includes both canonical and alias names):
320
+ {
321
+ "resourceId": "uuid",
322
+ "internalName": "Premium Coffee Beans",
323
+ "name": "Premium Coffee Beans",
324
+ "itemCode": "PREM-COFFEE",
325
+ "type": "PRODUCT",
326
+ "status": "ACTIVE"
327
+ }
328
+ ```
329
+
330
+ ### POST /api/v1/items
331
+
332
+ `name` alias is accepted (resolves to `internalName`).
333
+
334
+ ```json
335
+ // Request (either field name works):
336
+ {
337
+ "itemCode": "PREM-COFFEE",
338
+ "internalName": "Premium Coffee Beans",
339
+ "type": "PRODUCT",
340
+ "appliesToSale": true,
341
+ "saleItemName": "Premium Coffee Beans",
342
+ "salePrice": 45.00,
343
+ "saleAccountResourceId": "uuid",
344
+ "saleTaxProfileResourceId": "uuid",
345
+ "appliesToPurchase": true,
346
+ "purchaseItemName": "Premium Coffee Beans",
347
+ "purchasePrice": 25.00,
348
+ "purchaseAccountResourceId": "uuid",
349
+ "purchaseTaxProfileResourceId": "uuid"
350
+ }
351
+
352
+ // Response:
353
+ { "data": { "resourceId": "uuid", ... } }
354
+ ```
355
+
356
+ ---
357
+
358
+ ## 7. Invoices
359
+
360
+ ### POST /api/v1/invoices
361
+
362
+ ```json
363
+ // Request:
364
+ {
365
+ "contactResourceId": "uuid",
366
+ "saveAsDraft": false,
367
+ "reference": "INV-001",
368
+ "valueDate": "2026-02-08",
369
+ "dueDate": "2026-03-10",
370
+ "currency": "SGD",
371
+ "lineItems": [{
372
+ "name": "Premium Coffee Beans x 50",
373
+ "unitPrice": 45.00,
374
+ "quantity": 50,
375
+ "accountResourceId": "uuid",
376
+ "taxProfileResourceId": "uuid",
377
+ "itemResourceId": "uuid"
378
+ }]
379
+ }
380
+
381
+ // Response:
382
+ { "data": { "resourceId": "uuid", "reference": "INV-001" } }
383
+ ```
384
+
385
+ **saveAsDraft**: Defaults to `false` — omitting it creates a finalized transaction. Sending `saveAsDraft: true` creates a draft.
386
+
387
+ **GET response note**: When fetching invoices via GET, line items use `organizationAccountResourceId` (not `accountResourceId`). POST uses `accountResourceId`. Request-side aliases resolve `issueDate` → `valueDate`, `bankAccountResourceId` → `accountResourceId`, etc.
388
+
389
+ **FX (foreign currency) invoices**: For invoices in a non-base currency, use the `currency` OBJECT form:
390
+ - **`currency: { sourceCurrency: "MYR" }`** — platform auto-fetches rate from ECB (FRANKFURTER). Response shows `rateSource: "EXTERNAL"`, `providerName: "FRANKFURTER"`.
391
+ - **`currency: { sourceCurrency: "MYR", exchangeRate: 3.15 }`** — custom rate. Response shows `rateSource: "INTERNAL_TRANSACTION"`, `providerName: "CUSTOM"`.
392
+ - **`currencyCode: "MYR"` (string) is SILENTLY IGNORED** — the invoice is created in the org's base currency (e.g., SGD) with rate 1:1. No error returned. This is a major gotcha.
393
+ - **`currency: "USD"` (string)** causes "Invalid request body" error (400).
394
+
395
+ **Rate hierarchy** (when using `currency: { sourceCurrency }` without `exchangeRate`):
396
+ 1. Org-level rate (set via `/organization-currencies/:code/rates`) — auto-filled if exists
397
+ 2. Platform rate (ECB via FRANKFURTER) — auto-fetched if no org rate
398
+ 3. Transaction-level rate (via `exchangeRate` in the `currency` object) — overrides all
399
+
400
+ **Invoice payments**: `POST /invoices/{invoiceResourceId}/payments` works reliably as a standalone endpoint.
401
+
402
+ ---
403
+
404
+ ## 8. Bills
405
+
406
+ ### POST /api/v1/bills
407
+
408
+ Same structure as invoices. All field names identical. FX currency rules also apply — use `currency` object form (see Section 7 FX notes).
409
+
410
+ **Bill payments**: The standalone `POST /bills/{id}/payments` endpoint was broken (nil pointer dereference in the API backend) — **fixed in backend PR #112**. Both standalone and embedded payment approaches now work. The embed-in-creation pattern remains a valid alternative:
411
+
412
+ ```json
413
+ // Request — Bill with embedded payment:
414
+ {
415
+ "contactResourceId": "uuid",
416
+ "saveAsDraft": false,
417
+ "reference": "BILL-001",
418
+ "valueDate": "2026-02-08",
419
+ "dueDate": "2026-03-10",
420
+ "lineItems": [{
421
+ "name": "Office Supplies",
422
+ "unitPrice": 500.00,
423
+ "quantity": 1,
424
+ "accountResourceId": "uuid",
425
+ "taxProfileResourceId": "uuid"
426
+ }],
427
+ "payments": [{
428
+ "paymentAmount": 500.00,
429
+ "transactionAmount": 500.00,
430
+ "accountResourceId": "uuid-of-bank-account",
431
+ "paymentMethod": "BANK_TRANSFER",
432
+ "reference": "PAY-BILL-001",
433
+ "valueDate": "2026-02-08"
434
+ }]
435
+ }
436
+ ```
437
+
438
+ The `payments` array uses the same 6-field structure as standalone payments. production clients uses embedded payments for bills.
439
+
440
+ **Note**: Bill payments do NOT support `TransactionFeeCollected` (model field missing). Only invoice payments support collected transaction fees.
441
+
442
+ ### Withholding Tax on Line Items
443
+
444
+ Bills and supplier credit notes support withholding tax per line item:
445
+
446
+ ```json
447
+ {
448
+ "lineItems": [{
449
+ "name": "Consulting services",
450
+ "unitPrice": 10000,
451
+ "quantity": 1,
452
+ "accountResourceId": "expense-uuid",
453
+ "withholdingTax": {
454
+ "code": "WC010",
455
+ "rate": 10,
456
+ "description": "Professional fees"
457
+ }
458
+ }]
459
+ }
460
+ ```
461
+
462
+ **Retry pattern**: If the organization doesn't support withholding tax, the API returns `WITHHOLDING_CODE_NOT_FOUND`. On this error, remove the `withholdingTax` field from all line items and retry the request.
463
+
464
+ ---
465
+
466
+ ## 9. Journals
467
+
468
+ ### POST /api/v1/journals
469
+
470
+ ```json
471
+ // Request:
472
+ {
473
+ "saveAsDraft": false,
474
+ "reference": "JV-001",
475
+ "valueDate": "2026-02-08",
476
+ "journalEntries": [
477
+ { "accountResourceId": "uuid", "amount": 500, "type": "DEBIT", "contactResourceId": "uuid" },
478
+ { "accountResourceId": "uuid", "amount": 500, "type": "CREDIT" }
479
+ ]
480
+ }
481
+
482
+ // Response:
483
+ { "data": { "resourceId": "uuid" } }
484
+ ```
485
+
486
+ **CRITICAL corrections from live testing**:
487
+ - Each entry uses `amount` (number) + `type`: `"DEBIT"` or `"CREDIT"` (UPPERCASE strings)
488
+ - Do NOT use `debit`/`credit` as separate number fields — that is WRONG
489
+ - Do NOT include `currency` at top level — causes "Invalid request body"
490
+ - Total DEBIT amounts MUST equal total CREDIT amounts
491
+ - `contactResourceId` is optional per entry
492
+
493
+ ---
494
+
495
+ ## 10. Cash Entries
496
+
497
+ ### POST /api/v1/cash-in-journals
498
+ ### POST /api/v1/cash-out-journals
499
+
500
+ ```json
501
+ // Request — Cash-In example:
502
+ {
503
+ "saveAsDraft": false,
504
+ "reference": "CI-001",
505
+ "valueDate": "2026-02-08",
506
+ "accountResourceId": "uuid-of-bank-account",
507
+ "journalEntries": [
508
+ { "accountResourceId": "uuid-of-revenue-account", "amount": 500, "type": "CREDIT" }
509
+ ]
510
+ }
511
+
512
+ // Request — Cash-Out example:
513
+ {
514
+ "saveAsDraft": false,
515
+ "reference": "CO-001",
516
+ "valueDate": "2026-02-08",
517
+ "accountResourceId": "uuid-of-bank-account",
518
+ "journalEntries": [
519
+ { "accountResourceId": "uuid-of-expense-account", "amount": 300, "type": "DEBIT" }
520
+ ]
521
+ }
522
+
523
+ // Response:
524
+ { "data": { "resourceId": "uuid" } }
525
+ ```
526
+
527
+ **CRITICAL corrections from live testing**:
528
+ - `saveAsDraft` is REQUIRED — omitting it causes validation failure
529
+ - `accountResourceId` at top level = the BANK account (NOT `bankAccountResourceId`)
530
+ - `journalEntries` array for the offset entries — same `amount` + `type` format as regular journals
531
+ - Do NOT use a flat structure with `amount`, `bankAccountResourceId`, `description` — that is WRONG
532
+ - The system auto-creates the bank-side entry; you only specify the offset entries in `journalEntries`
533
+ - For cash-in: offset entries are typically CREDIT (revenue/liability)
534
+ - For cash-out: offset entries are typically DEBIT (expense/asset)
535
+
536
+ ---
537
+
538
+ ## 11. Payments
539
+
540
+ ### POST /api/v1/invoices/{invoiceResourceId}/payments (WORKS)
541
+ ### POST /api/v1/bills/{billResourceId}/payments (FIXED in PR #112)
542
+
543
+ ```json
544
+ // Request:
545
+ {
546
+ "payments": [{
547
+ "paymentAmount": 2250.00,
548
+ "transactionAmount": 2250.00,
549
+ "accountResourceId": "uuid-of-bank-account",
550
+ "paymentMethod": "BANK_TRANSFER",
551
+ "reference": "PAY-001",
552
+ "valueDate": "2026-02-05"
553
+ }]
554
+ }
555
+
556
+ // Response:
557
+ { "data": { "resourceIds": ["uuid"] } }
558
+ ```
559
+
560
+ **CRITICAL corrections from live testing** — payments require 6 fields:
561
+ - `paymentAmount` — NOT `amount`. The **bank account currency** amount (actual cash moved from bank).
562
+ - `transactionAmount` — The **transaction document currency (invoice/bill/credit note)** amount (applied to the balance). Equal to `paymentAmount` for same-currency. For cross-currency (e.g., USD invoice paid from SGD bank at 1.35): `paymentAmount: 1350` (SGD), `transactionAmount: 1000` (USD).
563
+ - `accountResourceId` — NOT `bankAccountResourceId`. This IS the bank account UUID.
564
+ - `paymentMethod` — required string: `"BANK_TRANSFER"` (other values may exist but this works universally)
565
+ - `reference` — payment reference string (required)
566
+ - `valueDate` — NOT `paymentDate`. ISO date string.
567
+
568
+ Always wrap in `{ payments: [...] }` even for single payment.
569
+
570
+ **Bill payments standalone endpoint**: Was broken (nil pointer dereference) — **fixed in backend PR #112**. Now works for basic payments. Embed-in-creation pattern also remains valid (see Section 8). production clients uses embedded payments for bills.
571
+
572
+ **TransactionFeeCollected**: NOT supported on bill payments (model field missing in the API backend). Only invoice payments support collected transaction fees.
573
+
574
+ ---
575
+
576
+ ## 12. Credit Notes
577
+
578
+ ### POST /api/v1/customer-credit-notes
579
+ ### POST /api/v1/supplier-credit-notes
580
+
581
+ ```json
582
+ // Request (same structure for both):
583
+ {
584
+ "contactResourceId": "uuid",
585
+ "saveAsDraft": false,
586
+ "reference": "CN-001",
587
+ "valueDate": "2026-02-08",
588
+ "lineItems": [{
589
+ "name": "Return - Defective items",
590
+ "unitPrice": 45.00,
591
+ "quantity": 5,
592
+ "accountResourceId": "uuid",
593
+ "taxProfileResourceId": "uuid"
594
+ }]
595
+ }
596
+
597
+ // Response:
598
+ { "data": { "resourceId": "uuid" } }
599
+ ```
600
+
601
+ ### POST /api/v1/invoices/{invoiceResourceId}/credits
602
+
603
+ ```json
604
+ // Request:
605
+ {
606
+ "credits": [
607
+ { "creditNoteResourceId": "uuid-of-credit-note", "amountApplied": 225.00 }
608
+ ]
609
+ }
610
+ ```
611
+
612
+ **CRITICAL corrections from live testing**:
613
+ - Wrap in `credits` array (NOT a flat object)
614
+ - Use `amountApplied` (NOT `amount`)
615
+ - Can apply multiple credit notes in one call by adding more entries to the array
616
+
617
+ ---
618
+
619
+ ## 13. Tags
620
+
621
+ ### GET /api/v1/tags?limit=100&offset=0
622
+ ### POST /api/v1/tags
623
+
624
+ `name` alias is accepted (resolves to `tagName`).
625
+
626
+ ```json
627
+ // Request (either field name works):
628
+ { "tagName": "Department: Sales" }
629
+ // or: { "name": "Department: Sales" }
630
+
631
+ // Response (includes both canonical and alias names):
632
+ { "data": { "tagName": "Department: Sales", "name": "Department: Sales", "status": "ACTIVE", "resourceId": "uuid" } }
633
+ ```
634
+
635
+ ---
636
+
637
+ ## 14. Custom Fields
638
+
639
+ ### GET /api/v1/custom-fields?limit=100&offset=0
640
+ ### POST /api/v1/custom-fields
641
+
642
+ ```json
643
+ // Request (TEXT type):
644
+ { "name": "PO Number", "type": "TEXT", "printOnDocuments": false }
645
+
646
+ // Request (DROPDOWN type with options):
647
+ { "name": "Priority", "type": "DROPDOWN", "printOnDocuments": false, "options": ["Low", "Medium", "High"] }
648
+
649
+ // Request (DATE type):
650
+ { "name": "Delivery Date", "type": "DATE", "printOnDocuments": true }
651
+
652
+ // Response (includes both canonical and alias names):
653
+ { "data": { "customFieldName": "PO Number", "name": "PO Number", "status": "ACTIVE", "resourceId": "uuid" } }
654
+ ```
655
+
656
+ **CRITICAL notes from live testing**:
657
+ - POST uses `name`, GET returns both `customFieldName` and `name`
658
+ - Valid `type` values: `"TEXT"`, `"DATE"`, `"DROPDOWN"` (UPPERCASE)
659
+ - `printOnDocuments` is REQUIRED — omitting it causes 400
660
+ - Do NOT send `appliesTo` field — causes "Invalid request body"
661
+ - Only send: `name`, `type`, `printOnDocuments` (and `options` for DROPDOWN)
662
+ - For DROPDOWN type, `options` array works (without `appliesTo`)
663
+
664
+ ---
665
+
666
+ ## 14b. Inventory Items
667
+
668
+ ### POST /api/v1/inventory-items
669
+
670
+ ```json
671
+ // Request:
672
+ {
673
+ "internalName": "Widget A",
674
+ "itemCode": "WDG-A",
675
+ "unit": "pcs",
676
+ "appliesToSale": true,
677
+ "appliesToPurchase": true,
678
+ "saleItemName": "Widget A",
679
+ "salePrice": 50.00,
680
+ "saleAccountResourceId": "uuid-operating-revenue",
681
+ "saleTaxProfileResourceId": "uuid-tax",
682
+ "purchaseItemName": "Widget A",
683
+ "purchasePrice": 30.00,
684
+ "purchaseAccountResourceId": "uuid-inventory-account",
685
+ "purchaseTaxProfileResourceId": "uuid-tax",
686
+ "costingMethod": "WAC",
687
+ "cogsResourceId": "uuid-direct-costs",
688
+ "blockInsufficientDeductions": false,
689
+ "inventoryAccountResourceId": "uuid-inventory-account"
690
+ }
691
+
692
+ // Response:
693
+ { "data": { "resourceId": "uuid" } }
694
+ ```
695
+
696
+ **CRITICAL notes from live testing**:
697
+ - `unit` is REQUIRED (e.g., `"pcs"`, `"box"`, `"kg"`) — omitting causes ITEM_UNIT_EMPTY_ERROR
698
+ - `costingMethod` must be `"FIXED"` or `"WAC"` (NOT `"FIXED_COST"`)
699
+ - `purchaseAccountResourceId` MUST point to an Inventory-type CoA account (NOT Direct Costs) — wrong type causes INVALID_ACCOUNT_TYPE_INVENTORY
700
+ - `inventoryAccountResourceId` also must be Inventory-type
701
+ - Delete inventory items via `DELETE /items/:id` (NOT `/inventory-items/:id`)
702
+ - `GET /inventory-item-balance/:id` returns balance per item
703
+ - `GET /inventory-balances/:status` currently returns 500 (known bug)
704
+
705
+ ---
706
+
707
+ ## 14c. Cash Transfer Journals
708
+
709
+ ### POST /api/v1/cash-transfer-journals
710
+
711
+ ```json
712
+ // Request:
713
+ {
714
+ "valueDate": "2026-02-09",
715
+ "saveAsDraft": false,
716
+ "reference": "XFER-001",
717
+ "cashOut": { "accountResourceId": "uuid-from-bank", "amount": 500 },
718
+ "cashIn": { "accountResourceId": "uuid-to-bank", "amount": 500 }
719
+ }
720
+
721
+ // Response:
722
+ { "data": { "resourceId": "uuid" } }
723
+ ```
724
+
725
+ **CRITICAL**: Uses `cashOut`/`cashIn` sub-objects — NOT `fromAccountResourceId`/`toAccountResourceId`/`amount` flat fields. Each sub-object has `accountResourceId` and `amount`.
726
+
727
+ ---
728
+
729
+ ## 14d. Credit Note Refunds
730
+
731
+ ### POST /api/v1/customer-credit-notes/{id}/refunds
732
+
733
+ ```json
734
+ // Request:
735
+ {
736
+ "refunds": [{
737
+ "refundAmount": 75.00,
738
+ "refundMethod": "BANK_TRANSFER",
739
+ "transactionAmount": 75.00,
740
+ "accountResourceId": "uuid-bank",
741
+ "reference": "REFUND-001",
742
+ "valueDate": "2026-02-09"
743
+ }]
744
+ }
745
+
746
+ // Response:
747
+ { "data": { "resourceIds": ["uuid"] } }
748
+ ```
749
+
750
+ **CRITICAL**: Uses `refunds` wrapper with `refundAmount`/`refundMethod` — NOT `payments` wrapper with `paymentAmount`/`paymentMethod`.
751
+
752
+ ---
753
+
754
+ ## 14e. Contact Groups
755
+
756
+ ### POST /api/v1/contact-groups
757
+
758
+ ```json
759
+ // Request:
760
+ { "name": "VIP Clients", "description": "Top-tier customers" }
761
+
762
+ // Response:
763
+ { "data": { "resourceId": "uuid" } }
764
+ ```
765
+
766
+ **Known bug**: `PUT /contact-groups/:id` returns 500. Use create + delete as workaround.
767
+
768
+ ---
769
+
770
+ ## 14f. Organization Bookmarks
771
+
772
+ ### POST /api/v1/organization/bookmarks
773
+
774
+ ```json
775
+ // Request:
776
+ {
777
+ "items": [{
778
+ "name": "Company Policy",
779
+ "value": "https://example.com/policy",
780
+ "categoryCode": "GENERAL_INFORMATION",
781
+ "datatypeCode": "LINK"
782
+ }]
783
+ }
784
+
785
+ // Response:
786
+ { "data": [{ "name": "Company Policy", "resourceId": "uuid" }] }
787
+ ```
788
+
789
+ Valid `categoryCode`: `AUDIT_AND_ASSURANCE`, `BANKING_AND_FINANCE`, `BUDGETS_AND_CONTROLS`, `EMPLOYEES_AND_PAYROLL`, `EXTERNAL_DOCUMENTS`, `GENERAL_INFORMATION`, `OWNERS_AND_DIRECTORS`, `TAXATION_AND_COMPLIANCE`, `WORKFLOWS_AND_PROCESSES`.
790
+
791
+ Valid `datatypeCode`: `TEXT`, `NUMBER`, `BOOLEAN`, `DATE`, `LINK`.
792
+
793
+ ---
794
+
795
+ ## 15. Bank Records
796
+
797
+ ### POST /api/v1/magic/importBankStatementFromAttachment (multipart)
798
+
799
+ The only endpoint for creating bank records. Uses multipart form upload:
800
+
801
+ ```
802
+ POST /api/v1/magic/importBankStatementFromAttachment
803
+ Content-Type: multipart/form-data
804
+
805
+ Fields:
806
+ - sourceFile: CSV/OFX bank statement file (NOT "file")
807
+ - accountResourceId: UUID of the bank account CoA entry (NOT "bankAccountResourceId")
808
+ - businessTransactionType: "BANK_STATEMENT"
809
+ - sourceType: "FILE" (valid values: URL, FILE)
810
+ ```
811
+
812
+ CSV format: `Date,Description,Debit,Credit` — maps to Date, Description, Cash-out, Cash-in.
813
+
814
+ Multipart import is the more reliable method. Use it when JSON POST returns errors.
815
+
816
+ ---
817
+
818
+ ## 16. Schedulers
819
+
820
+ ### POST /api/v1/scheduled/invoices
821
+
822
+ ```json
823
+ // Request:
824
+ {
825
+ "repeat": "MONTHLY",
826
+ "startDate": "2026-03-01",
827
+ "endDate": "2026-12-01",
828
+ "invoice": {
829
+ "contactResourceId": "uuid",
830
+ "saveAsDraft": false,
831
+ "reference": "SCH-INV-001",
832
+ "valueDate": "2026-03-01",
833
+ "dueDate": "2026-03-31",
834
+ "lineItems": [{
835
+ "name": "Monthly retainer",
836
+ "unitPrice": 3000.00,
837
+ "quantity": 1,
838
+ "accountResourceId": "uuid",
839
+ "taxProfileResourceId": "uuid"
840
+ }]
841
+ }
842
+ }
843
+ ```
844
+
845
+ ### POST /api/v1/scheduled/bills
846
+
847
+ Same but with `"bill"` wrapper instead of `"invoice"`.
848
+
849
+ ### POST /api/v1/scheduled/journals
850
+
851
+ ```json
852
+ // Request (FLAT structure — NOT nested in "journal" wrapper):
853
+ {
854
+ "reference": "SCHED-JNL-001",
855
+ "valueDate": "2026-03-01",
856
+ "saveAsDraft": false,
857
+ "schedulerEntries": [
858
+ { "accountResourceId": "uuid", "amount": 100, "type": "DEBIT", "name": "Monthly accrual" },
859
+ { "accountResourceId": "uuid", "amount": 100, "type": "CREDIT", "name": "Monthly accrual" }
860
+ ],
861
+ "repeat": "MONTHLY",
862
+ "startDate": "2026-03-01",
863
+ "endDate": "2026-12-01"
864
+ }
865
+
866
+ // Response:
867
+ { "data": { "resourceId": "uuid" } }
868
+ ```
869
+
870
+ **CRITICAL**: Scheduled journals use FLAT structure with `schedulerEntries` — NOT a nested `journal` wrapper like scheduled invoices/bills use `invoice`/`bill` wrapper. `reference`, `valueDate`, `saveAsDraft` are at top level alongside `repeat`/`startDate`/`endDate`.
871
+
872
+ **CRITICAL notes from live testing**:
873
+ - Recurrence field is `repeat` — NOT `frequency` or `interval`. Using `frequency` or `interval` silently defaults to ONE_TIME.
874
+ - Valid `repeat` values: `"WEEKLY"`, `"MONTHLY"`, `"QUARTERLY"`, `"YEARLY"`
875
+ - `saveAsDraft: false` is REQUIRED on the wrapped invoice/bill. Using `saveAsDraft: true` causes `INVALID_SALE_STATUS` (invoices) or `INVALID_PURCHASE_STATUS` (bills).
876
+ - Since `saveAsDraft: false`, every line item MUST have `accountResourceId`.
877
+ - Response uses `interval` field (not `repeat`): `{ "status": "ACTIVE", "interval": "MONTHLY", ... }`
878
+
879
+ ---
880
+
881
+ ## 17. Reports
882
+
883
+ ### POST /api/v1/generate-reports/trial-balance
884
+
885
+ ```json
886
+ // Request:
887
+ { "startDate": "2025-11-10", "endDate": "2026-02-08" }
888
+ ```
889
+
890
+ Both dates required.
891
+
892
+ ### POST /api/v1/generate-reports/balance-sheet
893
+
894
+ ```json
895
+ // Request:
896
+ { "primarySnapshotDate": "2026-02-28" }
897
+ ```
898
+
899
+ Uses `primarySnapshotDate` — NOT `endDate`. Optional: `secondarySnapshotDates` array for comparison periods.
900
+
901
+ ### POST /api/v1/generate-reports/profit-and-loss
902
+
903
+ ```json
904
+ // Request:
905
+ { "primarySnapshotDate": "2026-02-28", "secondarySnapshotDate": "2026-01-01" }
906
+ ```
907
+
908
+ Both `primarySnapshotDate` and `secondarySnapshotDate` required. NOT `startDate`/`endDate`.
909
+
910
+ ### POST /api/v1/generate-reports/general-ledger
911
+
912
+ ```json
913
+ // Request:
914
+ { "startDate": "2026-01-01", "endDate": "2026-02-28", "groupBy": "ACCOUNT" }
915
+ ```
916
+
917
+ `groupBy` is required. Valid values: `"ACCOUNT"`. Uses `startDate`/`endDate` like trial balance.
918
+
919
+ ### POST /api/v1/generate-reports/cashflow
920
+
921
+ ```json
922
+ { "primaryStartDate": "2026-01-01", "primaryEndDate": "2026-02-28" }
923
+ ```
924
+
925
+ Uses `primaryStartDate`/`primaryEndDate` — NOT `primarySnapshotDate`.
926
+
927
+ ### POST /api/v1/generate-reports/cash-balance
928
+
929
+ ```json
930
+ { "reportDate": "2026-02-28" }
931
+ ```
932
+
933
+ Single date field `reportDate`.
934
+
935
+ ### POST /api/v1/generate-reports/ar-report
936
+ ### POST /api/v1/generate-reports/ap-report
937
+
938
+ ```json
939
+ { "endDate": "2026-02-28" }
940
+ ```
941
+
942
+ Single date field `endDate`.
943
+
944
+ ### POST /api/v1/generate-reports/ar-summary-report
945
+ ### POST /api/v1/generate-reports/ap-summary-report
946
+
947
+ ```json
948
+ { "startDate": "2026-01-01", "endDate": "2026-02-28" }
949
+ ```
950
+
951
+ Both `startDate` and `endDate` required.
952
+
953
+ ### POST /api/v1/generate-reports/bank-balance-summary
954
+
955
+ ```json
956
+ { "primarySnapshotDate": "2026-02-28" }
957
+ ```
958
+
959
+ ### POST /api/v1/generate-reports/equity-movement
960
+
961
+ ```json
962
+ { "primarySnapshotStartDate": "2026-01-01", "primarySnapshotEndDate": "2026-02-28" }
963
+ ```
964
+
965
+ Uses `primarySnapshotStartDate`/`primarySnapshotEndDate` — yet another pair of field names.
966
+
967
+ ### Data Exports
968
+
969
+ Data exports use SIMPLER field names than generate-reports:
970
+
971
+ | Export | Fields |
972
+ |--------|--------|
973
+ | `/data-exports/trial-balance` | `startDate`, `endDate` |
974
+ | `/data-exports/profit-and-loss` | `startDate`, `endDate` |
975
+ | `/data-exports/general-ledger` | `startDate`, `endDate`, `groupBy: "ACCOUNT"` |
976
+ | `/data-exports/ar-report` | `endDate` |
977
+
978
+ **Note**: P&L export uses `startDate`/`endDate` (NOT `primarySnapshotDate`/`secondarySnapshotDate` like generate-reports).
979
+
980
+ ---
981
+
982
+ ## 18. Cashflow Transactions Search
983
+
984
+ ### POST /api/v1/cashflow-transactions/search
985
+
986
+ Searches across ALL cashflow transactions (invoices, bills, credit notes, journals, cash entries, payments). This is the unified transaction ledger.
987
+
988
+ ```json
989
+ // Request:
990
+ {
991
+ "filter": {
992
+ "businessTransactionType": { "eq": "SALE" },
993
+ "valueDate": { "gte": "2026-01-01" }
994
+ },
995
+ "sort": { "sortBy": ["valueDate"], "order": "DESC" },
996
+ "limit": 100
997
+ }
998
+
999
+ // Response (flat, same as all other search endpoints):
1000
+ {
1001
+ "totalElements": 1228,
1002
+ "totalPages": 13,
1003
+ "data": [{
1004
+ "resourceId": "uuid",
1005
+ "direction": "PAYIN",
1006
+ "totalAmount": 2250.00,
1007
+ "balanceAmount": 0,
1008
+ "grossAmount": 2250.00,
1009
+ "feeAmount": 0,
1010
+ "valueDate": 1706227200000,
1011
+ "matchDate": 1706313600000,
1012
+ "businessTransactionType": "SALE",
1013
+ "businessTransactionReference": "INV-001",
1014
+ "businessTransactionStatus": "POSTED",
1015
+ "currencyCode": "SGD",
1016
+ "currencySymbol": "S$",
1017
+ "functionalCurrencyCode": "SGD",
1018
+ "crossCurrency": false,
1019
+ "contact": { "name": "Acme Corp", "resourceId": "uuid" },
1020
+ "account": { "name": "Accounts Receivable", "resourceId": "uuid" },
1021
+ "organizationAccountResourceId": "uuid",
1022
+ "tags": ["Department: Sales"]
1023
+ }]
1024
+ }
1025
+ ```
1026
+
1027
+ **Response shape**: Standard flat `{ totalElements, totalPages, data: [...] }` (same as all search/list endpoints).
1028
+
1029
+ **CRITICAL response dates**: `valueDate` and `matchDate` are `int64` epoch milliseconds (e.g., `1706227200000`), NOT `YYYY-MM-DD` strings. Convert: `new Date(epochMs).toISOString().slice(0, 10)`.
1030
+
1031
+ **Valid `businessTransactionType` values**: `SALE`, `PURCHASE`, `SALE_CREDIT_NOTE`, `PURCHASE_CREDIT_NOTE`, `JOURNAL_MANUAL`, `JOURNAL_DIRECT_CASH_IN`, `JOURNAL_DIRECT_CASH_OUT`, `JOURNAL_CASH_TRANSFER`, `FIXED_ASSET`.
1032
+
1033
+ **Valid `direction` values**: `PAYIN`, `PAYOUT`.
1034
+
1035
+ For full filter/sort field reference, see `references/search-reference.md` section 10.
1036
+
1037
+ ---
1038
+
1039
+ ## 19. Bank Records Search
1040
+
1041
+ ### POST /api/v1/bank-records/:accountResourceId/search
1042
+
1043
+ Searches bank statement entries for a specific bank account. The `accountResourceId` path parameter is the UUID of a bank-type CoA account — find it via `POST /chart-of-accounts/search` with `{ "filter": { "accountType": { "eq": "Bank Accounts" } } }`.
1044
+
1045
+ ```json
1046
+ // Request:
1047
+ {
1048
+ "filter": {
1049
+ "status": { "eq": "UNRECONCILED" },
1050
+ "valueDate": { "gte": "2026-01-01" }
1051
+ },
1052
+ "sort": { "sortBy": ["valueDate"], "order": "DESC" },
1053
+ "limit": 100
1054
+ }
1055
+
1056
+ // Response:
1057
+ {
1058
+ "totalElements": 280,
1059
+ "totalPages": 3,
1060
+ "data": [{
1061
+ "resourceId": "uuid",
1062
+ "description": "Payment from Acme Corp",
1063
+ "netAmount": 2500.00,
1064
+ "valueDate": 1706227200000,
1065
+ "status": "UNRECONCILED",
1066
+ "extContactName": "Acme Corp",
1067
+ "extReference": "TXN-12345",
1068
+ "extAccountNumber": "****1234"
1069
+ }]
1070
+ }
1071
+ ```
1072
+
1073
+ **Valid `status` values**: `RECONCILED`, `UNRECONCILED`, `ARCHIVED`, `POSSIBLE_DUPLICATE`.
1074
+
1075
+ For full filter/sort field reference, see `references/search-reference.md` section 11.
1076
+
1077
+ ---
1078
+
1079
+ ## 20. Bank Records — JSON POST (Alternative)
1080
+
1081
+ ### POST /api/v1/bank-records/:accountResourceId
1082
+
1083
+ In addition to multipart import (Section 15), bank records can be created via JSON POST:
1084
+
1085
+ ```json
1086
+ // Request:
1087
+ {
1088
+ "records": [{
1089
+ "description": "Payment from client",
1090
+ "payerOrPayee": "Acme Corp",
1091
+ "reference": "TXN-001",
1092
+ "amount": 2500.00,
1093
+ "transactionDate": "2026-02-10",
1094
+ "metadata": []
1095
+ }]
1096
+ }
1097
+
1098
+ // Response:
1099
+ { "data": { "resourceIds": ["uuid1"] } }
1100
+ ```
1101
+
1102
+ **Fields**:
1103
+ - `records` (required): Array of 1-100 records
1104
+ - `amount` (required): Positive = cash-in, negative = cash-out
1105
+ - `transactionDate` (required): `YYYY-MM-DD` format
1106
+ - `description`, `payerOrPayee`, `reference`: Optional strings (max 65536 chars)
1107
+ - `metadata`: Optional array of `{ index, name, value }` objects (max 100)
1108
+
1109
+ **When to use**: JSON POST is best for programmatic creation. Multipart import (`POST /magic/importBankStatementFromAttachment`) is best for CSV/OFX file uploads.
1110
+
1111
+ ---
1112
+
1113
+ ## Advanced Search (POST /*/search)
1114
+
1115
+ All resources support `POST /api/v1/{resource}/search` with filter syntax. **For per-endpoint filter/sort field lists, see `references/search-reference.md`.**
1116
+
1117
+ ### Request Example
1118
+ ```json
1119
+ POST /api/v1/invoices/search
1120
+ {
1121
+ "filter": {
1122
+ "status": { "eq": "POSTED" },
1123
+ "valueDate": { "between": ["2026-01-01", "2026-12-31"] }
1124
+ },
1125
+ "sort": {
1126
+ "sortBy": ["valueDate"],
1127
+ "order": "DESC"
1128
+ },
1129
+ "limit": 100,
1130
+ "offset": 0
1131
+ }
1132
+ ```
1133
+
1134
+ ### Filter Operators
1135
+ | Type | Operators |
1136
+ |------|----------|
1137
+ | String | `eq`, `neq`, `contains`, `in` (max 100), `reg` (max 100), `likeIn` (max 100), `isNull` |
1138
+ | Numeric | `eq`, `gt`, `gte`, `lt`, `lte`, `in` (max 100) |
1139
+ | Date | `eq`, `gt`, `gte`, `lt`, `lte`, `between` (exactly 2 YYYY-MM-DD values) |
1140
+ | DateTime | Same as Date but RFC3339 format (for `createdAt`/`updatedAt`) |
1141
+ | Boolean | `eq` |
1142
+ | Logical | `and`, `or`, `not` (nested objects), `andGroup`, `orGroup` (arrays, invoices/bills/journals only) |
1143
+
1144
+ ### Pagination
1145
+ - `limit`: max 1000 per page (default 100)
1146
+ - `offset`: skip N results (max 65536)
1147
+ - `sort`: **REQUIRED when `offset` is present** (even `offset: 0`)
1148
+ - Response includes `totalElements` and `totalPages`
1149
+
1150
+ ---
1151
+
1152
+ ## Catalogs (Experimental)
1153
+
1154
+ > Endpoint availability varies by organization. Use try/catch — if all requests fail, the endpoint may not be enabled.
1155
+
1156
+ ### Create Catalog
1157
+ POST /api/v1/catalogs
1158
+ ```json
1159
+ {
1160
+ "name": "Premium Products",
1161
+ "itemResourceIds": ["uuid-1", "uuid-2"],
1162
+ "description": "Curated product catalog for VIP customers"
1163
+ }
1164
+ ```
1165
+
1166
+ ### Response
1167
+ ```json
1168
+ { "data": { "resourceId": "catalog-uuid" } }
1169
+ ```
1170
+
1171
+ ---
1172
+
1173
+ ## Deposits (Experimental)
1174
+
1175
+ > Endpoint availability varies by organization. Use try/catch.
1176
+
1177
+ ### Create Deposit
1178
+ POST /api/v1/deposits
1179
+ ```json
1180
+ {
1181
+ "contactResourceId": "contact-uuid",
1182
+ "depositDate": "2026-02-01",
1183
+ "amount": 5000.00,
1184
+ "type": "AR",
1185
+ "bankAccountResourceId": "bank-account-uuid",
1186
+ "currencyCode": "SGD"
1187
+ }
1188
+ ```
1189
+
1190
+ - `type`: `"AR"` (accounts receivable / customer deposit) or `"AP"` (accounts payable / supplier deposit)
1191
+ - `depositDate`: YYYY-MM-DD format
1192
+ - `bankAccountResourceId`: Must be a CoA entry with accountType "Bank Accounts"
1193
+
1194
+ ### Response
1195
+ ```json
1196
+ { "data": { "resourceId": "deposit-uuid" } }
1197
+ ```
1198
+
1199
+ ---
1200
+
1201
+ ## Fixed Assets (Experimental)
1202
+
1203
+ > Endpoint availability varies by organization. Use try/catch.
1204
+
1205
+ ### Create Fixed Asset
1206
+ POST /api/v1/fixed-assets
1207
+ ```json
1208
+ {
1209
+ "name": "Office Laptop - MacBook Pro",
1210
+ "purchaseDate": "2026-01-15",
1211
+ "purchaseCost": 3500.00,
1212
+ "depreciationMethod": "STRAIGHT_LINE",
1213
+ "usefulLifeMonths": 36,
1214
+ "assetAccountResourceId": "fixed-asset-coa-uuid",
1215
+ "depreciationAccountResourceId": "depreciation-coa-uuid",
1216
+ "expenseAccountResourceId": "expense-coa-uuid",
1217
+ "currencyCode": "SGD"
1218
+ }
1219
+ ```
1220
+
1221
+ - `depreciationMethod`: `"STRAIGHT_LINE"` (uppercase)
1222
+ - `usefulLifeMonths`: Integer
1223
+ - All three account fields must reference valid CoA entries
1224
+
1225
+ ### Response
1226
+ ```json
1227
+ { "data": { "resourceId": "asset-uuid" } }
1228
+ ```
1229
+
1230
+ ---
1231
+
1232
+ ## Inventory Adjustments (Experimental)
1233
+
1234
+ > Endpoint availability varies by organization. Use try/catch.
1235
+
1236
+ ### Create Adjustment
1237
+ POST /api/v1/inventory/adjustments
1238
+ ```json
1239
+ {
1240
+ "itemResourceId": "inventory-item-uuid",
1241
+ "quantity": 50,
1242
+ "adjustmentDate": "2026-02-01",
1243
+ "reason": "Initial stock count",
1244
+ "accountResourceId": "inventory-coa-uuid"
1245
+ }
1246
+ ```
1247
+
1248
+ - `quantity`: Positive integer (adjustment amount)
1249
+ - `adjustmentDate`: YYYY-MM-DD format
1250
+ - `itemResourceId`: Must reference an inventory-type item (not service)
1251
+
1252
+ ### Response
1253
+ ```json
1254
+ { "data": { "resourceId": "adjustment-uuid" } }
1255
+ ```
1256
+
1257
+ ---
1258
+
1259
+ ---
1260
+
1261
+ ## Field Aliases (Create/Update Endpoints)
1262
+
1263
+ Middleware on create and update endpoints transparently maps alias field names to canonical names. Both forms are accepted — the alias is only applied if the canonical field is absent.
1264
+
1265
+ | Alias | Canonical | Endpoints |
1266
+ |-------|-----------|-----------|
1267
+ | `issueDate` | `valueDate` | Invoices, bills, credit notes, journals, cash entries, cash transfers, all scheduled create/update endpoints |
1268
+ | `date` | `valueDate` | Same as above (including scheduled endpoints) |
1269
+ | `paymentDate` | `valueDate` | Payments (invoice/bill payments) |
1270
+ | `bankAccountResourceId` | `accountResourceId` | Payments |
1271
+ | `paymentAmount` | `refundAmount` | Credit note refunds |
1272
+ | `paymentMethod` | `refundMethod` | Credit note refunds |
1273
+ | `name` | `tagName` | Tags (create, update) |
1274
+ | `name` | `internalName` | Items (create) |
1275
+ | `accountType` | `classificationType` | Chart of accounts (create, update, bulk-upsert) |
1276
+ | `currencyCode` | `currency` | Chart of accounts bulk-upsert |
1277
+
1278
+ **Note**: Aliases apply only to POST/PUT request bodies. Search filter fields use their canonical names (e.g., `tagName` not `name` in `POST /tags/search`).
1279
+
1280
+ ---
1281
+
1282
+ ## Auto-Wrapping (NormalizeToArray)
1283
+
1284
+ Middleware on payment, credit, and refund endpoints automatically wraps a flat JSON object into an array. Both formats work:
1285
+
1286
+ | Endpoint | Array format (preferred) | Flat format (auto-wrapped) |
1287
+ |----------|------------------------|---------------------------|
1288
+ | `POST /invoices/:id/payments` | `{ "payments": [{...}] }` | `{ "paymentAmount": ..., ... }` → auto-wrapped to `{ "payments": [{...}] }` |
1289
+ | `POST /bills/:id/payments` | `{ "payments": [{...}] }` | Same |
1290
+ | `POST /invoices/:id/credits` | `{ "credits": [{...}] }` | Same |
1291
+ | `POST /bills/:id/credits` | `{ "credits": [{...}] }` | Same |
1292
+ | `POST /customer-credit-notes/:id/refunds` | `{ "refunds": [{...}] }` | Same |
1293
+ | `POST /supplier-credit-notes/:id/refunds` | `{ "refunds": [{...}] }` | Same |
1294
+
1295
+ **Recommendation**: Always use the array format for clarity and consistency.
1296
+
1297
+ ---
1298
+
1299
+ *Last updated: 2026-02-14 — All search/list responses standardized to flat shape. Scheduled endpoints support date aliases. Sections: cashflow-transactions/search (18), bank-records search (19), bank-records JSON POST (20). Complete filter operator reference. Per-endpoint details in search-reference.md.*