Journal Entries (v2)
Create a journal entry (v2)
Credential: a company's API key or OAuth token.
Create a journal entry. For CUSTOMER_INVOICE / CUSTOMER_PAYMENT / SUPPLIER_INVOICE / SUPPLIER_PAYMENT,
submit only the counter-side lines (revenue, expense, bank, etc.). The server appends the AR/AP leg
on the customer's or vendor's group default account, with side derived from the sign of originalAmount
(positive = invoice/payment, negative = credit-note/refund). Including a line on the group default
account explicitly will be rejected.
For all other journal types this endpoint behaves identically to v1: pass through whatever lines you supply.
Attachments: include up to 10 base64-encoded files in the optional attachments[] field — same shape and
limits as v1. Each becomes a Document linked to the created entry and surfaces via
GET /api/v1/journal-entries/:id/attachments. Rejection paths: an invalid MIME type rejects at DTO
validation with 400 SHARED_VALIDATION_FAILED; malformed base64 reaches the service and rejects with
400 JOURNAL_ENTRY_INVALID_ATTACHMENT_TYPE. Total request body must stay under 15MB.
Required Scope
journal-entries-write
Headers
x-api-idempotency-keyOptional idempotency key for safely retrying mutating requests.
Create a journal entry (v2) › Request Body
entryTypeJournal type. For CUSTOMER_INVOICE / CUSTOMER_PAYMENT / SUPPLIER_INVOICE / SUPPLIER_PAYMENT the AR/AP leg is appended automatically; do not include it in lines.
datePosting date (YYYY-MM-DD).
originalAmountFace value of the document — what the counterparty is invoiced or pays. NOT the sum of lines. On CUSTOMER_INVOICE / CUSTOMER_PAYMENT / SUPPLIER_INVOICE / SUPPLIER_PAYMENT this value IS the appended AR/AP leg (its absolute value), so an account that appears on both sides of the entry — a clearing or pass-through account — must not be counted into it. Sign carries direction: positive = invoice / payment, negative = credit-note / refund.
Counter-side lines (revenue / expense / bank / outlay). For locked types do NOT include the AR/AP leg. The cap is per ENTRY, not per request or per journal category — nothing accumulates across entries, so a company with many unposted drafts is never refused for that reason.
documentDateDocument date (YYYY-MM-DD): the date printed on the invoice or voucher. The VAT period follows it, so a supplier invoice dated 28 March and booked on 3 April lands in March. Omit it and the entry has no document date of its own; its VAT period then follows date. null is read as omitted.
dueDateDue date (YYYY-MM-DD) of a customer or supplier invoice, used exactly as sent. Omit it and the due date is calculated from the customer's or supplier's payment term (else the company default), counted from documentDate when sent and from date otherwise. Accepted only on customer_invoice, supplier_invoice and the deprecated invoice, and only with a reference, which is what registers the invoice the due date belongs to. Sent on any other entry type, it is refused with 400 JOURNAL_ENTRY_DUE_DATE_NOT_ALLOWED; sent without a reference, with 400 JOURNAL_ENTRY_DUE_DATE_REQUIRES_REFERENCE. null is read as omitted.
customerIdCustomer reference. Required for CUSTOMER_* types.
supplierIdSupplier reference. Required for SUPPLIER_* types.
descriptionOptional human-readable description.
referenceInvoice number / external reference. On CUSTOMER_PAYMENT it names the invoice being paid, and the appended receivable leg follows where that invoice was booked: on the one account it sits on, which is the customer group account unless the invoice lines sent the receivable elsewhere (a deposit invoice). An invoice owed across several receivable accounts is cleared one leg per account when the payment pays everything it still has open; a part payment of such an invoice is refused with JOURNAL_ENTRY_RECEIVABLE_SPLIT_REQUIRED, because nothing here states which account it clears and it is never divided in proportion.
externalSystemIdYour own system's id for this entry. Leading and trailing whitespace is trimmed before storage, and a value that is only whitespace is stored as no id at all — the list filter trims what it is given too, so an untrimmed value would be stored under an id no lookup could match. It is a lookup key, not a deduplication key: LedgerBee enforces no uniqueness on it and never compares it between requests, so several entries may carry the same id and repeating a request creates another entry. Filter GET /journal-entries by externalSystemId to find what an earlier run posted. For retries that must not post twice, send x-api-idempotency-key.
originalCurrency^[A-Za-z]{3}$ISO 4217 alpha-3 code of the currency the entry is posted in; originalAmount and lines[].amount are in this currency. Omitted under both names, the entry is posted in your company base currency.
originalCurrencyToBaseCurrencyExchangeRateWhat one unit of originalCurrency is worth in your company base currency, e.g. 7.4612 for EUR to DKK. Must be greater than zero; a non-positive rate is rejected whatever the entry currency, so the same body is accepted or refused identically for every tenant. Omit it and exchangeRate to have the rate resolved from the rate feed at the posting date — supplying one lets an integrator that already converted the amount itself post both legs of a settlement against the same rate. Not applied when the entry is already denominated in the company base currency, where the conversion is the identity. A read of the posted entry returns the rate under this name, and again under the deprecated exchangeRate.
tagIdsTag IDs applied at the entry level.
journalCategoryIdJournal category ID (UUID). Must belong to the API key's app company; cross-tenant or unknown IDs are rejected with 400 JOURNAL_CATEGORY_NOT_FOUND. When omitted the entry uses the company's default category.
Inline file attachments (base64-encoded). Same shape and limits as v1: maximum 10 attachments per entry, a 10MB ceiling on each file measured AFTER base64 decoding (over it is rejected with DOCUMENT_UPLOAD_FILE_TOO_LARGE), and a 15MB ceiling on the total request body (JSON + base64 payload). Either can be the binding one: base64 inflates a payload by about a third, so two 6MB files each clear the per-file bound and exceed the request one. Each attachment becomes a Document linked to the created journal entry — equivalent to what GET /api/v1/journal-entries/:id/attachments returns. An invalid MIME type rejects at DTO validation with 400 SHARED_VALIDATION_FAILED; malformed base64 reaches the service and rejects with 400 JOURNAL_ENTRY_INVALID_ATTACHMENT_TYPE. Either rejects the whole entry, and in a batch the whole batch rolls back.
vendorIdDeprecated — use supplierId. Legacy alias; sending both with different values returns 400.
currency^[A-Za-z]{3}$Deprecated — use originalCurrency. The same currency code under its old name. Sending both with different codes returns 400 JOURNAL_ENTRY_DEPRECATED_FIELD_CONFLICT.
exchangeRateDeprecated — use originalCurrencyToBaseCurrencyExchangeRate. The same rate under its old name. Sending both with different values returns 400 JOURNAL_ENTRY_DEPRECATED_FIELD_CONFLICT.
Create a journal entry (v2) › Responses
Journal entry created successfully
idUnique identifier of the created journal entry
entryNumberEntry number assigned to the journal entry
dateDate of the journal entry
amountAmount of the line accountId names, in that line's account currency (currency). On a two-line entry this is the entry total. On a wider entry it is one line of several — sum lines[].functionalAmount on either side for the total.
currencyCurrency amount is denominated in — the operating currency of that first line's account.
journalTypeJournal type
journalCategoryIdID of the journal category the entry is in. A draft booked with a category keeps it. List the categories with GET /v1/journal-categories. Null when the entry has no category.
typeTransaction type
statusStatus of the journal entry
reversedByEntryIdThe id of the entry that reverses this one, or null when no reversal made in LedgerBee points at it. A reversed entry keeps its status, so this field is what says it no longer counts: it and its reversal cancel each other out. A reversal imported from e-conomic cancels its entry through opposite amounts and is not linked, so the entry it cancels reads null here.
reversalOfEntryIdOn a reversal, the id of the entry it reverses; null on every other entry. The entry named here names this one in its reversedByEntryId.
accountIdOne non-VAT line of the entry, paired with contraAccountId as a two-line summary. On a two-line entry the pair describes the whole entry. On a wider one it describes two of its lines and nothing about the rest — read lines for the full picture. Which two are picked is not part of this contract and differs by endpoint (a read takes them in posting order; the draft-booking response takes a debit and a credit), so do not infer a line's position from it.
createdAtTimestamp when the entry was created
documentDateDocument date (date on the invoice/voucher). Null when the entry was booked without one; its VAT period then follows date.
dueDateDue date of the customer or supplier invoice this entry books (YYYY-MM-DD). Null on every other entry type, on a credit note booked without one, and on an invoice posted through this API without a reference (no invoice is registered for it).
originalAmountWhat you posted, in the currency you posted it in — the document figure, before any conversion. Distinct from amount, which is denominated in the ACCOUNT currency (currency): a EUR entry against DKK accounts reports originalAmount: 200 / originalCurrency: "EUR" and amount: 1492.24 / currency: "DKK". Null on historical entries that stored no document amount.
originalCurrencyThe currency the entry was posted in: what originalAmount is denominated in, and what originalCurrencyToBaseCurrencyExchangeRate converts FROM. Present independently of originalAmount — an entry can name a currency and store no document amount, which is the historical shape, and the currency still says what its rate converts from. Null only on an entry that names no currency at all.
originalCurrencyToBaseCurrencyExchangeRateRate converting originalCurrency to the company base currency — 7.45 on a EUR entry of a DKK-based company means 1 EUR = 7.45 DKK, so originalAmount * originalCurrencyToBaseCurrencyExchangeRate is the base-currency total that lines[].functionalAmount sums to. Reported whether you supplied it on create or the platform resolved it, which is what lets you compute the difference against the rate your own settlement later used. 1 on an entry posted in the base currency, since that is the conversion that was applied and stored. Null only on historical entries that stored no rate at all.
originalCurrencyToBaseCurrencyExchangeRateDateDate the originalCurrencyToBaseCurrencyExchangeRate was captured for. Set on every posted entry, including one that stored no rate; an entry posted in your company base currency carries its own date.
descriptionDescription of the journal entry
referenceReference number
externalSystemIdYour own system's id for this entry, when set. Not unique — several entries may share one id.
contraAccountIdThe other half of the accountId pair, when the entry has a second non-VAT line. Same caveat: it is one line, not a summary of every line other than accountId.
Every line of the entry, including VAT lines the platform derived. This is the complete picture: accountId, contraAccountId, amount, currency and vatCodeId above all describe one line, or two, and nothing about the rest. Debits and credits balance across the array on functionalAmount. The order is stable for a given entry — normally the order the lines were posted in — but match lines by account rather than by position, and use vatSourceLineIndex to link a VAT line to its source.
customerIdCustomer ID if applicable
supplierIdSupplier ID if applicable
vatCodeIdVAT/Tax code on the line accountId names, if it has one. Lines can carry different codes — read lines[].vatCodeId for the per-line codes.
tagIdsTag IDs attached to the entry as a whole. Per-line dimensions are on lines[].tagIds.
documentRefsDocument IDs attached to the entry
Notes on the posted entry, as codes. Present only when there is one. JOURNAL_ENTRY_YEAR_END_ACCOUNT_POSTED: the entry has a line on the company year-end account and every other line is on a balance-sheet account; details.accountId names the account. An entry with a line there and an income or expense line is refused with 400 JOURNAL_ENTRY_YEAR_END_ACCOUNT_RESULT_LINE.
exchangeRateDeprecated — use originalCurrencyToBaseCurrencyExchangeRate, which carries the same value.
exchangeRateDateDeprecated — use originalCurrencyToBaseCurrencyExchangeRateDate, which carries the same value.
vendorIdDeprecated — use supplierId. Legacy alias, emitted with the same value.
Create journal entries in batch (v2)
Credential: a company's API key or OAuth token.
Atomically creates 1–100 journal entries in a single transaction. Each entry follows the same restrictions as POST /api/v2/journal-entries: for CUSTOMER_/SUPPLIER_ types submit only the counter-side lines and the server auto-appends the AR/AP leg on the partner group default account.
If any entry fails validation or persistence the entire batch is rolled back. journalCategoryId is validated per entry against the API key's app company — cross-tenant or unknown IDs reject the whole batch with 400 JOURNAL_CATEGORY_NOT_FOUND.
Attachments: each entry may include up to 10 base64-encoded attachments[]. A failure on any entry
rolls back the whole batch and deletes every already-uploaded blob — no orphan documents survive.
Required Scope
journal-entries-write
Headers
x-api-idempotency-keyOptional idempotency key for safely retrying mutating requests.
Create journal entries in batch (v2) › Request Body
Array of 1–100 journal entries, carrying at most 10000 lines in total across the batch. Wrapped in a single transaction; one bad entry rolls back the whole batch.
Create journal entries in batch (v2) › Responses
Batch created successfully
totalCreatedNumber of journal entries created (= entries.length on success).
Created journal entries, ordered to match the request entries[].