Journals
Manual journal entries: create, submit through the approval policy, reverse.
| Operation | Summary | Scope | Idempotency-Key |
|---|---|---|---|
GET /v1/journal-entries |
List Entries | read |
|
POST /v1/journal-entries |
Create a journal entry | draft |
required |
GET /v1/journal-entries/{entry_id} |
Get a journal entry | read |
|
PUT /v1/journal-entries/{entry_id} |
Change a draft entry | draft |
required |
DELETE /v1/journal-entries/{entry_id} |
Delete a draft entry | draft |
|
POST /v1/journal-entries/{entry_id}/reverse |
Reverse a posted entry | post |
required |
POST /v1/journal-entries/{entry_id}/submit |
Submit a draft entry | draft |
GET /v1/journal-entries
List Entries
Journal entries of all statuses with their lines, newest first (by entry date, then by
creation time). All filters are optional and combine; account needs entity, else 422.
| Required scope | read (admin implies every scope) |
| Operation id | list_entries_v1_journal_entries_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string or null | Only entries of this entity (code or id). | |
| query | status |
string or null | Only entries with this status: draft, pending_approval, posted or rejected. |
|
| query | source |
string or null | Only entries created by this source: manual or the subledger or process that made them, e.g. ar, ap, fa, bank, fx, close. |
|
| query | start |
string (date) or null | Only entries dated on or after this day (ISO 8601). | |
| query | end |
string (date) or null | Only entries dated on or before this day (ISO 8601). | |
| query | q |
string or null | Keep entries whose description or entry number contains this text (case-insensitive). | |
| query | account |
string or null | Only entries with a line on this account code (needs entity). |
|
| query | min_amount |
decimal string or null | Only entries whose debits total at least this amount, in the entity's functional currency (decimal, e.g. 1000.00). (min 0) | |
| query | limit |
integer | How many entries to return, newest first; at most 1000. (max 1000; default 100) |
Example request
curl "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of JournalEntryOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"entry_date": "2026-09-21",
"description": "string",
"source": "string",
"source_ref": "string",
"reverses_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reversed_by_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"auto_reverse_on": "2026-09-21",
"adjustment_period": 1,
"provenance": {},
"hash": "string",
"prev_hash": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"posted_by": {},
"posted_at": "2026-09-21T09:30:00Z",
"lines": [
{
"line_no": 1,
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"description": null,
"currency": "string",
"debit": "string",
"credit": "string",
"fx_rate": "string",
"functional_debit": "string",
"functional_credit": "string",
"tax_code": null,
"tax": null,
"fx": null,
"counterparty_type": null,
"counterparty_id": null,
"subledger_type": null,
"subledger_id": null,
"dimensions": {}
}
]
}
]
POST /v1/journal-entries
Create a journal entry
Create a manual journal entry and, unless submit is false, submit it.
The entry must balance (debits equal credits; on a multi-currency entry an FX rounding
difference of up to 0.05 in functional currency is booked to the rounding account; a larger
one answers 422 entry_off_in_currency with the limit) and its date must fall in an open
period. A rate typed on a line (fx_rate) more than 10% away from the reference rate (ECB)
of the entry date answers 422 fx_rate_implausible naming both rates and the date, unless
confirm_rate is true; without a reference rate for the currency or day it is not checked.
Control accounts (bank, receivables, payables, fixed assets) cannot be used: they are
posted through their subledgers.
source and provenance are set by the server; sending others answers 422. The approval
policy decides (result.policy): allow posts it at once (result.status posted, with
its number), require_approval leaves it pending_approval with an approval_id, and
deny answers 403. An entry made by a key without the post scope always waits for
approval. With submit false the status stays draft and policy shows what would
happen on submit.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_journal_entry_v1_journal_entries_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | submit |
boolean | true (default): submit the entry through the approval policy at once. false: keep it as a draft; submit it later with POST /v1/journal-entries/{entry_id}/submit. (default true) |
Request body
application/json (required): EntryInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code or id. |
entry_date |
string (date) | yes | The posting date, ISO 8601 (e.g. 2026-09-30); it decides the fiscal period, which must be open. |
description |
string | yes | What the entry is for; shown in the journal and the general ledger. At least one character. (min length 1) |
lines |
array of LineInput |
yes | The lines of the entry, at least two. Debits and credits must balance. (min items 2) |
source |
string | Where the entry comes from. Over the API it is always manual: omit it. Subledger entries are created by their own endpoints. (default "manual") |
|
source_ref |
string or null | Your own reference for the entry, e.g. a document number in another system; free text, null when omitted. | |
provenance |
object | Free-form JSON that records where the entry came from. Over the API it must be empty: omit it. | |
auto_reverse_on |
string (date) or null | Reverse the entry on this date (e.g. the first day of the next period for an accrual). The reversal is posted when the entry is posted, also after an approval; the date must be after entry_date, in an open period. | |
confirm_rate |
boolean | Use the fx_rate typed on a line even when it differs by more than 10% from the reference rate (ECB) of the entry date. Without it such a line is refused (422 fx_rate_implausible); with it the line's fx records the confirmation and the reference rate. (default false) |
|
adjustment_period |
integer or null | Post into this adjustment period (e.g. 13) instead of the regular period; entry_date must be the last day of the fiscal year. (min 1; max 15) |
Example request
curl -X POST "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"description": "Accrued audit fee September 2026",
"entity": "CH01",
"entry_date": "2026-09-30",
"lines": [
{
"account": "6500",
"debit": "1500.00",
"description": "Audit fee"
},
{
"account": "2300",
"credit": "1500.00",
"description": "Accrual"
}
]
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | SubmitOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"result": {
"entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "string",
"number": "string",
"policy": {
"decision": "allow",
"reason": "string",
"rule_id": "string",
"table_id": "string"
},
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reason": "string"
},
"entry": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"entry_date": "2026-09-21",
"description": "string",
"source": "string",
"source_ref": "string",
"reverses_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reversed_by_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"auto_reverse_on": "2026-09-21",
"adjustment_period": 1,
"provenance": {},
"hash": "string",
"prev_hash": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"posted_by": {},
"posted_at": "2026-09-21T09:30:00Z",
"lines": [
{
"line_no": null,
"account_id": null,
"description": null,
"currency": null,
"debit": null,
"credit": null,
"fx_rate": null,
"functional_debit": null,
"functional_credit": null,
"tax_code": null,
"tax": null,
"fx": null,
"counterparty_type": null,
"counterparty_id": null,
"subledger_type": null,
"subledger_id": null,
"dimensions": null
}
]
}
}
GET /v1/journal-entries/{entry_id}
Get a journal entry
One journal entry with its lines, in any status. A posted entry carries its number and
its place in the hash chain (hash, prev_hash).
| Required scope | read (admin implies every scope) |
| Operation id | get_entry_v1_journal_entries__entry_id__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entry_id |
string (uuid) | yes | Journal entry id. |
Example request
curl "$ERP_URL/v1/journal-entries/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | JournalEntryOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"entry_date": "2026-09-21",
"description": "string",
"source": "string",
"source_ref": "string",
"reverses_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reversed_by_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"auto_reverse_on": "2026-09-21",
"adjustment_period": 1,
"provenance": {},
"hash": "string",
"prev_hash": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"posted_by": {},
"posted_at": "2026-09-21T09:30:00Z",
"lines": [
{
"line_no": 1,
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"description": "string",
"currency": "string",
"debit": "string",
"credit": "string",
"fx_rate": "string",
"functional_debit": "string",
"functional_credit": "string",
"tax_code": "string",
"tax": {},
"fx": {},
"counterparty_type": "string",
"counterparty_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"subledger_type": "string",
"subledger_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"dimensions": {}
}
]
}
PUT /v1/journal-entries/{entry_id}
Change a draft entry
Replace the content of a draft entry: its date, description, lines, auto_reverse_on,
adjustment_period (the body is the same as on create, and the whole entry is sent). It is
checked exactly as a new entry is (balance, open period, accounts, reversal date). The entry
keeps its id, created_by and created_at; the audit trail records the change
(journal.draft_updated, before and after), and whoever changed it cannot approve its
posting (four eyes).
Only a draft entered by hand (source manual) is changed: 409 draft_not_editable when
the entry waits for approval, is posted or rejected, and 409 draft_from_process for a
draft that a process made (FX revaluation, depreciation, revenue recognition and others),
which is corrected by that process.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | update_draft_v1_journal_entries__entry_id__put |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entry_id |
string (uuid) | yes | Journal entry id. |
| query | submit |
boolean | false (default): keep the changed entry as a draft. true: submit it through the approval policy at once, as POST /v1/journal-entries/{entry_id}/submit. (default false) |
Request body
application/json (required): EntryInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code or id. |
entry_date |
string (date) | yes | The posting date, ISO 8601 (e.g. 2026-09-30); it decides the fiscal period, which must be open. |
description |
string | yes | What the entry is for; shown in the journal and the general ledger. At least one character. (min length 1) |
lines |
array of LineInput |
yes | The lines of the entry, at least two. Debits and credits must balance. (min items 2) |
source |
string | Where the entry comes from. Over the API it is always manual: omit it. Subledger entries are created by their own endpoints. (default "manual") |
|
source_ref |
string or null | Your own reference for the entry, e.g. a document number in another system; free text, null when omitted. | |
provenance |
object | Free-form JSON that records where the entry came from. Over the API it must be empty: omit it. | |
auto_reverse_on |
string (date) or null | Reverse the entry on this date (e.g. the first day of the next period for an accrual). The reversal is posted when the entry is posted, also after an approval; the date must be after entry_date, in an open period. | |
confirm_rate |
boolean | Use the fx_rate typed on a line even when it differs by more than 10% from the reference rate (ECB) of the entry date. Without it such a line is refused (422 fx_rate_implausible); with it the line's fx records the confirmation and the reference rate. (default false) |
|
adjustment_period |
integer or null | Post into this adjustment period (e.g. 13) instead of the regular period; entry_date must be the last day of the fiscal year. (min 1; max 15) |
Example request
curl -X PUT "$ERP_URL/v1/journal-entries/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"description": "Accrued audit fee September 2026",
"entity": "CH01",
"entry_date": "2026-09-30",
"lines": [
{
"account": "6500",
"debit": "1500.00",
"description": "Audit fee"
},
{
"account": "2300",
"credit": "1500.00",
"description": "Accrual"
}
]
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SubmitOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"result": {
"entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "string",
"number": "string",
"policy": {
"decision": "allow",
"reason": "string",
"rule_id": "string",
"table_id": "string"
},
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reason": "string"
},
"entry": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"entry_date": "2026-09-21",
"description": "string",
"source": "string",
"source_ref": "string",
"reverses_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reversed_by_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"auto_reverse_on": "2026-09-21",
"adjustment_period": 1,
"provenance": {},
"hash": "string",
"prev_hash": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"posted_by": {},
"posted_at": "2026-09-21T09:30:00Z",
"lines": [
{
"line_no": null,
"account_id": null,
"description": null,
"currency": null,
"debit": null,
"credit": null,
"fx_rate": null,
"functional_debit": null,
"functional_credit": null,
"tax_code": null,
"tax": null,
"fx": null,
"counterparty_type": null,
"counterparty_id": null,
"subledger_type": null,
"subledger_id": null,
"dimensions": null
}
]
}
}
DELETE /v1/journal-entries/{entry_id}
Delete a draft entry
Delete a draft entered by hand. A draft has no entry number yet, so no number is lost;
the audit trail keeps its date, description, amount and lines (journal.draft_deleted).
The same refusals as for a change: 409 draft_not_editable (not a draft; a posted entry is
reversed instead) and 409 draft_from_process (made by a process).
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | delete_draft_v1_journal_entries__entry_id__delete |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entry_id |
string (uuid) | yes | Journal entry id. |
Example request
curl -X DELETE "$ERP_URL/v1/journal-entries/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 204 | No content | |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
POST /v1/journal-entries/{entry_id}/reverse
Reverse a posted entry
Reverse a posted manual entry or opening entry: the same lines with debit and credit
swapped, dated on (default: the entry's own date). The reason is required (at most 500
characters) and recorded on the reversal entry and in the audit trail. It goes through the
approval policy (action reverse_entry): only a person reverses, never an agent (403).
A person who created, posted or approved the entry reverses it at once and gets the reversal
entry (200; reverses_id names the original, whose reversed_by_id is now set). If another
person created, posted or approved the entry, or the policy limits what one person posts, the
reversal waits for a second person: answer 202 with the approval request, decided with
POST /v1/approvals/{approval_id}/approve (four eyes; everything is checked again then). A
request that nobody decides blocks the close of the entry's period (the close checklist counts
it) until it is approved or rejected.
Only manual entries and opening entries are reversed here. The opening entry of a data import
is the import's correction path (reverse it, then import the balances again) and always waits
for a second person (202); it leaves the open items and the clearing account in place, so the
close check opening_clearing fails until the balances are imported again. Entries of a
subledger or process (invoices, credit notes, bills, payments, depreciation, FX revaluation,
bank postings, VAT settlement, intercompany, revenue recognition, year-end close) are refused
with 409, and the message names where the correction belongs, so that documents, payments and
assets never drift from the ledger. Also 409 when the entry is not posted, is already
reversed, already has a reversal waiting for approval, or the period of the reversal date is
closed; 422 when no fiscal period covers that date or the reason is missing.
| Required scope | post (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | reverse_entry_v1_journal_entries__entry_id__reverse_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entry_id |
string (uuid) | yes | Journal entry id. |
Request body
application/json (required): ReverseRequest
| Field | Type | Required | Description |
|---|---|---|---|
on |
string (date) or null | Date of the reversal, ISO 8601; its period must exist and be open. Default: the date of the entry being reversed. | |
reason |
string | yes | Why it is reversed (required, at most 500 characters): appended to the description of the reversal entry and recorded in the audit trail and, when the reversal needs approval, in the approval request. (min length 1; max length 500) |
Example request
curl -X POST "$ERP_URL/v1/journal-entries/3fa85f64-5717-4562-b3fc-2c963f66afa6/reverse" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"on": "2026-10-01",
"reason": "Accrual released"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | JournalEntryOut |
| 202 | The reversal needs a second person: the approval request (object_type journal_reversal). The entry stays posted until it is approved. |
ApprovalOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"entry_date": "2026-09-21",
"description": "string",
"source": "string",
"source_ref": "string",
"reverses_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reversed_by_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"auto_reverse_on": "2026-09-21",
"adjustment_period": 1,
"provenance": {},
"hash": "string",
"prev_hash": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"posted_by": {},
"posted_at": "2026-09-21T09:30:00Z",
"lines": [
{
"line_no": 1,
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"description": "string",
"currency": "string",
"debit": "string",
"credit": "string",
"fx_rate": "string",
"functional_debit": "string",
"functional_credit": "string",
"tax_code": "string",
"tax": {},
"fx": {},
"counterparty_type": "string",
"counterparty_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"subledger_type": "string",
"subledger_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"dimensions": {}
}
]
}
POST /v1/journal-entries/{entry_id}/submit
Submit a draft entry
Submit a draft entry through the approval policy: it is posted if the policy allows it
for the caller (result.status posted), otherwise it becomes pending_approval with an
approval_id for a second person. Only drafts can be submitted (409 otherwise). A policy
decision deny answers 403. A key without the post scope always sends it to approval.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | submit_entry_v1_journal_entries__entry_id__submit_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entry_id |
string (uuid) | yes | Journal entry id. |
Example request
curl -X POST "$ERP_URL/v1/journal-entries/3fa85f64-5717-4562-b3fc-2c963f66afa6/submit" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SubmitOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"result": {
"entry_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "string",
"number": "string",
"policy": {
"decision": "allow",
"reason": "string",
"rule_id": "string",
"table_id": "string"
},
"approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reason": "string"
},
"entry": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"number": "string",
"status": "string",
"entry_date": "2026-09-21",
"description": "string",
"source": "string",
"source_ref": "string",
"reverses_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reversed_by_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"auto_reverse_on": "2026-09-21",
"adjustment_period": 1,
"provenance": {},
"hash": "string",
"prev_hash": "string",
"created_by": {},
"created_at": "2026-09-21T09:30:00Z",
"posted_by": {},
"posted_at": "2026-09-21T09:30:00Z",
"lines": [
{
"line_no": null,
"account_id": null,
"description": null,
"currency": null,
"debit": null,
"credit": null,
"fx_rate": null,
"functional_debit": null,
"functional_credit": null,
"tax_code": null,
"tax": null,
"fx": null,
"counterparty_type": null,
"counterparty_id": null,
"subledger_type": null,
"subledger_id": null,
"dimensions": null
}
]
}
}
Schemas
EntryInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code or id. |
entry_date |
string (date) | yes | The posting date, ISO 8601 (e.g. 2026-09-30); it decides the fiscal period, which must be open. |
description |
string | yes | What the entry is for; shown in the journal and the general ledger. At least one character. (min length 1) |
lines |
array of LineInput |
yes | The lines of the entry, at least two. Debits and credits must balance. (min items 2) |
source |
string | Where the entry comes from. Over the API it is always manual: omit it. Subledger entries are created by their own endpoints. (default "manual") |
|
source_ref |
string or null | Your own reference for the entry, e.g. a document number in another system; free text, null when omitted. | |
provenance |
object | Free-form JSON that records where the entry came from. Over the API it must be empty: omit it. | |
auto_reverse_on |
string (date) or null | Reverse the entry on this date (e.g. the first day of the next period for an accrual). The reversal is posted when the entry is posted, also after an approval; the date must be after entry_date, in an open period. | |
confirm_rate |
boolean | Use the fx_rate typed on a line even when it differs by more than 10% from the reference rate (ECB) of the entry date. Without it such a line is refused (422 fx_rate_implausible); with it the line's fx records the confirmation and the reference rate. (default false) |
|
adjustment_period |
integer or null | Post into this adjustment period (e.g. 13) instead of the regular period; entry_date must be the last day of the fiscal year. (min 1; max 15) |
JournalEntryOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id (uuid) of the entry; the {entry_id} of other operations. |
entity_id |
string (uuid) | yes | Id (uuid) of the entity. |
number |
string or null | yes | Entry number such as 2026-000042, assigned when it is posted; null before. |
status |
string | yes | draft, pending_approval (waits for a second person), posted (final) or rejected. |
entry_date |
string (date) | yes | The posting date (ISO 8601). |
description |
string | yes | What the entry is for. |
source |
string | yes | manual, or the subledger or process that created it: ar, ap, fa, bank, fx, close (the year-end closing entry), appropriation (a resolution on the result), ic (intercompany), rev (revenue recognition) or opening (a data import's opening entry). |
source_ref |
string or null | yes | The caller's own reference; null if none. |
reverses_id |
string (uuid) or null | yes | For a reversal, the id of the entry it reverses; else null. |
reversed_by_id |
string (uuid) or null | yes | Once reversed, the id of the reversal entry; else null. |
auto_reverse_on |
string (date) or null | The reversal is posted on this date when the entry posts. | |
adjustment_period |
integer or null | The adjustment period the entry is in (e.g. 13); null in a regular period. | |
provenance |
object | yes | Free-form JSON recording where the entry came from, e.g. the FX rates used or the document it was built from. |
hash |
string or null | yes | SHA-256 hash of the posted entry's content and prev_hash; null before posting. |
prev_hash |
string or null | yes | The hash of the previous posted entry of the entity (the hash chain); null for the first and before posting. |
created_by |
object | yes | The actor, as stored in the audit trail: an object with type (human, agent or system), id (user:<id>, key:<prefix> or system:<name>) and name. |
created_at |
string (date-time) | yes | When the entry was created (ISO 8601, UTC). |
posted_by |
object or null | yes | The actor that posted it, as created_by; null before posting. |
posted_at |
string (date-time) or null | yes | When it was posted (ISO 8601, UTC); null before. |
lines |
array of JournalLineOut |
The lines of the entry. |
JournalLineOut
| Field | Type | Required | Description |
|---|---|---|---|
line_no |
integer | yes | Position of the line in the entry, from 1. |
account_id |
string (uuid) | yes | Id (uuid) of the account (GET /v1/entities/{entity}/accounts). |
description |
string or null | yes | Text of the line; null if none. |
currency |
string | yes | ISO 4217 currency of the line. |
debit |
string | yes | Debit amount in the line's currency; 0 on a credit line. |
credit |
string | yes | Credit amount in the line's currency; 0 on a debit line. |
fx_rate |
string | yes | Units of the entity's functional currency per unit of currency; 1 when the line is in the functional currency. |
functional_debit |
string | yes | The debit in the entity's functional currency. |
functional_credit |
string | yes | The credit in the entity's functional currency. |
tax_code |
string or null | yes | VAT treatment code of the line; null if none. |
tax |
object or null | yes | VAT tag the VAT report reads (kind base or tax, direction, jurisdiction, treatment, rate); kind foreign_vat on a bill's lines that book the net amount and the foreign VAT the company cannot reclaim as cost, and non_deductible on the line that books the part of a bill line's input VAT that is not deductible as cost (recovery_percent), which no VAT report reads; null on lines without VAT. |
fx |
object or null | yes | Where the exchange rate came from (source, rate, fixing date); null in the functional currency. |
counterparty_type |
string or null | yes | customer or vendor; null if none. |
counterparty_id |
string (uuid) or null | yes | Id (uuid) of the customer or vendor; null if none. |
subledger_type |
string or null | yes | Kind of subledger record the line belongs to; null for manual lines. |
subledger_id |
string (uuid) or null | yes | Id (uuid) of that subledger record; null for manual lines. |
dimensions |
object | yes | The line's cost centre, profit centre, project ...: dimension code to value code. |
LineInput
| Field | Type | Required | Description |
|---|---|---|---|
account |
string | yes | Account code (or id) in the entry's entity. |
debit |
decimal string | Debit amount in the line's currency, a decimal string such as 1500.00; zero or omitted on a credit line. Exactly one of debit and credit must be above zero. (min 0; default "0") |
|
credit |
decimal string | Credit amount in the line's currency, a decimal string such as 1500.00; zero or omitted on a debit line. (min 0; default "0") |
|
currency |
string or null | Defaults to functional currency. | |
fx_rate |
decimal string or null | Units of functional currency per unit; default the ECB rate of the entry date. On a manual entry a rate more than 10% from that reference rate needs confirm_rate. (greater than 0) |
|
fx_meta |
object or null | Provenance of a supplied fx_rate (e.g. an ECB quote). | |
description |
string or null | Text of this line, shown in the general ledger; null when omitted. | |
tax_code |
string or null | VAT treatment code recorded on the line for reporting. Manual entries normally leave it out; the sales and purchase documents set it. | |
tax |
object or null | VAT tag the VAT report reads: an object with kind (base or tax), direction, jurisdiction, treatment and rate. Set by the sales and purchase documents; leave it out on manual entries. |
|
counterparty_type |
"customer" | "entity" | "vendor" or null |
customer or vendor, when the line concerns a customer or vendor; use with counterparty_id. entity (another entity of the organization) is set by the intercompany postings only; a manual entry is refused with it. |
|
counterparty_id |
string (uuid) or null | Id (uuid) of the customer or vendor the line concerns; null when omitted. | |
subledger_type |
string or null | Kind of subledger record the line belongs to. Set by the subledger services; leave it out on manual entries. | |
subledger_id |
string (uuid) or null | Id (uuid) of the subledger record the line belongs to. Set by the subledger services; leave it out on manual entries. | |
dimensions |
map of string | Dimension code to value code, e.g. {"cost_centre": "CC-100", "project": "P-17"} (GET /v1/dimensions); empty when omitted. Each must be an active value of an active dimension, valid on the entry date in its company (dimension_unknown, dimension_value_unknown); a manual entry gives every dimension its accounts require (dimension_required). |
SubmitOut
| Field | Type | Required | Description |
|---|---|---|---|
result |
SubmitResult |
yes | What the approval policy decided and where the entry stands now. |
entry |
JournalEntryOut |
yes | The journal entry in its new state, with its lines. |