List Journal Entries
GET/api/ledger/entries
Paginated journal entries listing. Discarded regeneration drafts are excluded; reversed entries stay visible so the reversal chain is browsable.
Any valid API token can call this endpoint — no specific scope required. Manage tokens.
Authentication: Requires Bearer token (Personal Access Token).
Filters (Spatie QueryBuilder):
filter[type]— exact entry type (AccountingTransactionTypeEnum, ~30 values, e.g. sales_order_invoice, fba_shipments, bill)filter[status]— exact lifecycle status: draft, posted, reversedfilter[display_status]— exact, comma-multi supported. Values: excluded, pending_sync, syncing, synced, update_pending, retrying, needs_attention, failed, removed_in_provider, conflict, voidedfilter[sync_routing]— Routing/cadence axis (individual | daily | monthly)filter[source_type]— exact source morph classfilter[batch_id]— exact batch IDfilter[date_from]/filter[date_to]— m/d/Y on effective_at (invalid dates silently ignored)filter[connection]— connection ID; entries holding a sync link on that connectionfilter[search]— partial match on reference, description, amount (total_debit), id, the resolved counterparty name and the synced provider document id (remote_id — e.g. a Xero invoice ID); scope withsearch_columns(e.g.search_columns=remote_id) and force exact match withsearch_strict_columns
Sorts: id, effective_at, reference, total_debit, created_at, type, counterparty, due_date, display_status, sync_routing (default: -effective_at). type sorts the stored entry-type enum; counterparty sorts the resolved supplier/customer/marketplace/provider name across polymorphic sources; due_date sorts the source purchase invoice's due date (other sources sort NULL).
Pagination: per_page default 10, clamped to max 100.
Each row carries a display_status object (key/label/color/reason). A batch member has no standalone status — its display_status mirrors its batch aggregate's live status at read time (Pending Sync / Syncing / Synced / Failed …); there is deliberately no batched status value. reason is the chip-tooltip hint: batch_member for batch members, comma-joined eligibility exclusion reasons (or no_accounting_connection) for excluded entries, null otherwise. Detail-only sections (lines, sync_links, operations, reversal_chain) are NOT included on the index — use the show endpoint.
Status is null for historical entries: the display_status and routing objects each report null key/label/color when an entry has left the live books — it is discarded (superseded by a regenerated replacement: replaced_by_id set + discarded_at) or a reversed original (status = reversed). Such rows can never sync (the batch rollup, reports and live lists all exclude them), so a frozen synced status would mislead; the supersession marker carries the why instead. A live reversal contra entry (reversal_of_id set, still posted) is NOT historical and keeps its status.
Advanced filters (filter_groups): base64-encoded JSON tree of AND/OR groups, applied via GroupedFilterSupport (same stack as Purchase Orders). Decoded shape:
{
"conjunction": "and",
"children": [
{"type": "condition", "condition": {"column": "reference", "operator": "contains", "value": "INV"}},
{"type": "group", "group": {"conjunction": "or", "children": [...]}}
]
}
Filterable columns: reference, description, entry_type, entry_status, entry_display_status, currency_code, counterparty (text: is, is_not, contains, does_not_contain, starts_with, ends_with, is_empty, is_not_empty, is_one_of); id, total_debit, total_credit, entry_batch_id (numeric: is, is_not, is_one_of, greater_than, less_than, greater_than_or_equal, less_than_or_equal, between, is_empty, is_not_empty); nominal_code_id (text, relationship — matches entries posting to the nominal code on any line); effective_at (date), posted_at, created_at (datetime: is, is_not, before, after, on_or_before, on_or_after, between, today, yesterday, past_week, past_month, …). counterparty has no stored column — it filters on the resolved supplier/customer/marketplace name via the same correlated subquery used for search and sort.
Unregistered columns in the tree are rejected with HTTP 400 (InvalidFilterQuery).
Each entry includes provider_link — the accounting-provider document it is synced to (the Xero/QBO column): { provider, remote_type, remote_id, link_type_label, label, link_route (internal detail page, or null when none exists yet), remote_url (provider deep link) }. It is null for entries not yet synced to a provider.
Per-lens columns (lens query param): the scoped Transactions tabs (Invoices / Journals / Purchase Orders) are thin wrappers over this one endpoint and request extra columns via ?lens=. The All tab, dashboard, detail and export paths send no lens, so the response keeps the shared columns only with zero extra queries. When a lens is present each row gains a derive-on-read block:
lens=invoicesaddscounterparty({ name, id, link_type_label, link_route }; a CRM party haslink_type_label"Supplier"/"Customer" + numericid+link_route; an AR source (sales order / sales credit) with no CRM customer falls back to a marketplace customer —name"{Integration} Customer" e.g. "Amazon Customer",link_type_label"Customer",id+link_routenull; a settlement source (Amazon financial-event group) falls back to a bare marketplace label —namethe integration name e.g. "Amazon",link_type_label"Marketplace",id+link_routenull; a ShipMyOrders provider invoice (a 3PL fulfilment-provider bill, no CRM party) falls back to a fixed provider label —name"SMO",link_type_label"Provider",id+link_routenull; null/absent when the source resolves no party or was deleted),due_date(YYYY-MM-DD date-only, no timezone conversion; null unless the source carries one — only purchase invoices do), andtax_total(scale-4 string summed across the entry's lines, or null when no line carries tax).lens=journalsaddsdebit_accountandcredit_account— each is either { split: false, nominal_code_id, code, name, link_route } for a side posting to one account, or { split: true, count } when the side fans out across several codes, or null when the side has no lines — plusbatch({ id, label, status, period_start, period_end, link_route } or null for unbatched entries).lens=purchase-ordersaddscounterparty(the supplier, same shape as above) andstage({ ordered: true, received: none|partial|full, invoiced: none|partial|full }) derived from the source purchase order's receipt_status / invoice_status.
Each row carries line_count — the number of GL-impact journal lines on the entry (cheap withCount subquery) — powering the entries table's inline expandable "Lines" column. Fetch the lines themselves via Get Journal Entry Lines (/api/ledger/entries/{entry}/lines).
can_sync_now (boolean) — whether a manual "Sync Now" can push this entry right now. The per-transaction-type Sync toggle, granularity opt-out and the per-type start date only gate automatic/scheduled sync; a manual push bypasses them, so can_sync_now stays true for a benign "Auto-off" entry. It is false only when a structural blocker applies (connection inactive, entry is a batch member / open batch, internal-only, or before the accounting provider's conversion/start date) — those block even a manual push. Drives the UI split between a grey "Auto-off · push anytime" chip (display_status=excluded + can_sync_now=true) and an amber "Blocked" chip (display_status=excluded + can_sync_now=false).
provider_status (response field) — the provider's own transaction status on the synced document (primary sync link remote_status); null for unsynced entries. Backs the Provider Status column + filter.
Request
Responses
- 200
- 401
- 403
- 429
OK
Response Headers
Unauthenticated — the bearer token is missing, revoked, expired, or malformed. Never retry automatically; fix the credential. See the Errors guide.
Forbidden — the token lacks a required scope, the endpoint is not available to API tokens, or the user behind the token lacks the permission. A human must adjust the token scopes or user permissions; do not retry.
Rate limited — platform limit is 1,000 requests/min; individual tokens may carry lower limits. Honor the Retry-After header before retrying. See the Rate Limits guide.