Reconciliations
Reconciliations are used to match and verify financial transactions across different accounts or statements. They help ensure the accuracy of accounting records by confirming that transactions are properly recorded and balanced.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /reconciliations | List reconciliations |
| POST | /reconciliations | Create a reconciliation |
| PATCH | /reconciliations/{id} | Approve a draft reconciliation |
| DELETE | /reconciliations/{id} | Delete a reconciliation |
Query Parameters
The GET /reconciliations endpoint supports the following query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | Filter by client account ID. Must be one of the user’s eligible client accounts |
| date_from | date | No | Filter entries from this date (inclusive) |
| date_to | date | No | Filter entries to this date (inclusive) |
| journal_entry_line_id | integer | No | Filter to reconciliations whose entries reference this journal entry line |
| journal_entry_id | integer | No | Filter to reconciliations whose entries belong to this journal entry |
| is_draft | boolean | No | Filter to draft proposals (true) or committed reconciliations (false). Omit to return both |
| is_deleted | boolean | No | Filter to soft-deleted (undone) reconciliations (true) or live ones (false). Defaults to false — unlike is_draft, omitting it does not return both |
| page | integer | No | Page number for pagination (default: 0) |
| per_page | integer | No | Number of items per page (default: 100) |
| with | string | No | Comma-separated list of relations to include. Supported: entries, entries.journal_entry_line, created_by |
Attributes
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the reconciliation |
| created_at | datetime | The date and time when the reconciliation was created |
| created_by_id | integer | The ID of the user that created the reconciliation |
| client_account_id | integer | The ID of the client account associated with the reconciliation |
| sequence_number | integer | The sequence number of the reconciliation. null while the reconciliation is a draft — sequence numbers are assigned only on commit |
| reconciliation_date | date | The date of the reconciliation |
| is_draft | boolean | Whether this is an unapproved proposal (true) or a committed reconciliation (false) |
| is_deleted | boolean | Whether the reconciliation has been soft-deleted (undone). Computed from deleted_at |
| deleted_at | datetime | When the reconciliation was deleted, or null if it hasn’t been |
| deleted_by_id | integer | The ID of the user who deleted the reconciliation, or null if it hasn’t been |
Relationships
All relationships are opt-in and must be requested via the with query parameter.
| Relationship | Type | Description |
|---|---|---|
| created_by | User | The user who created the reconciliation |
| entries | ReconciliationEntry | The entries in this reconciliation |
| entries.journal_entry_line | JournalEntryLine | The journal entry line behind each entry (request via with=entries.journal_entry_line) |
Reconciliation Entry
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the reconciliation entry |
| journal_entry_id | integer | The ID of the associated journal entry |
| journal_entry_line_id | integer | The ID of the associated journal entry line |
| debit | decimal | The debit amount in the base currency |
| debit_fc | decimal | The debit amount in foreign currency |
| credit | decimal | The credit amount in the base currency |
| credit_fc | decimal | The credit amount in foreign currency |
Example Request
GET /api/v2/reconciliations?client_account_id=7&with=entries,entries.journal_entry_line,created_by
Example Response
{
"data": [
{
"id": 1,
"created_at": "2023-05-10T12:00:00Z",
"created_by_id": 1,
"client_account_id": 7,
"sequence_number": 1,
"reconciliation_date": "2023-05-10",
"is_draft": false,
"is_deleted": false,
"deleted_at": null,
"deleted_by_id": null,
"entries": [
{
"id": 1,
"journal_entry_id": 1,
"journal_entry_line_id": 1,
"debit": "100.00",
"debit_fc": "100.00",
"credit": "0.00",
"credit_fc": "0.00",
"journal_entry_line": {
"id": 1,
"posting_date": "2023-05-10",
"description": "Bank transaction",
"account_code": "1920",
"account_id": 1,
"debit": "100.00",
"credit": "0.00"
}
}
],
"created_by": {
"id": 1,
"first_name": "John",
"last_name": "Doe"
}
}
],
"meta": {
"page": 0,
"pages": 1,
"per_page": 100,
"records": 1
}
}
Create Reconciliation
POST /api/v2/reconciliations
Creates a committed reconciliation matching two or more journal entry lines. The entries relation is included in the response by default.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID. Must be one of the user’s eligible accounts |
| date | date | Yes | The reconciliation date |
| entry_lines | array | Yes | At least two entries (see Entry line fields below) |
Entry line fields
| Field | Type | Required | Description |
|---|---|---|---|
| journal_entry_line_id | integer | Yes | The journal entry line being reconciled |
| journal_entry_id | integer | No | Defaults to the journal entry line’s own journal entry |
| debit | decimal | No | Debit amount in base currency. Defaults to the journal entry line’s debit/credit when every amount field is omitted |
| credit | decimal | No | Credit amount in base currency |
| debit_fc | decimal | No | Debit amount in foreign currency |
| credit_fc | decimal | No | Credit amount in foreign currency |
| exchange_rate | decimal | No | Defaults to the journal entry line’s exchange rate |
This endpoint always creates a committed reconciliation (is_draft: false) — is_draft cannot be set through the request body. Draft (proposal) reconciliations are produced only by Snapbooks’ internal automatic-matching pipeline and approved via Update Reconciliation.
Across all entries, local-currency (debit/credit) amounts must always balance to zero. If every entry shares one foreign currency and only the local-currency side is unbalanced, an offsetting FX gain/loss (agio) entry is posted automatically. If entries span more than one currency, foreign-currency amounts aren’t summed (mixing currencies is meaningless) — any local-currency residual must instead be balanced by an agio/disagio entry line supplied in the request; there’s no auto-posting for the cross-currency case.
For an entry that reconciles part of a journal entry line carrying a foreign-currency amount, debit_fc/credit_fc and debit/credit must preserve the line’s own implied rate (line.debit_or_credit / line.debit_fc_or_credit_fc) — not the line’s stored exchange_rate, which upstream postings sometimes fit to an external constraint. A 1-minor-unit rounding tolerance on the local-currency side absorbs frontend/backend rounding noise; anything past that is rejected rather than silently restated. Post a settlement-rate difference as a separate agio line instead of folding it into the entry.
Example Request
{
"client_account_id": 7,
"date": "2026-04-10",
"entry_lines": [
{ "journal_entry_line_id": 101, "debit": "0", "credit": "500.00" },
{ "journal_entry_line_id": 205, "debit": "500.00", "credit": "0" }
]
}
Response
Returns the created reconciliation with status 201 Created, including its entries.
{
"id": 42,
"created_at": "2026-04-10T09:15:00Z",
"created_by_id": 7,
"client_account_id": 7,
"sequence_number": 12,
"reconciliation_date": "2026-04-10",
"is_draft": false,
"entries": [
{
"id": 88,
"journal_entry_id": 501,
"journal_entry_line_id": 101,
"debit": "0.00",
"debit_fc": "0.00",
"credit": "500.00",
"credit_fc": "500.00"
},
{
"id": 89,
"journal_entry_id": 502,
"journal_entry_line_id": 205,
"debit": "500.00",
"debit_fc": "500.00",
"credit": "0.00",
"credit_fc": "0.00"
}
]
}
Error Responses
| Status | Description |
|---|---|
| 400 | Missing JSON body |
| 400 | Fewer than two entries, a duplicate journal_entry_line_id reference, an entry missing amounts, the entries don’t balance, an entry exceeds its journal entry line’s remaining (or unreconciled) amount, or an entry doesn’t preserve an FC-carrying line’s implied LC/FC scaling |
| 403 | Missing or invalid client_account_id, or no access to the client account |
Update Reconciliation
PATCH /api/v2/reconciliations/{id}
Approves a draft reconciliation, committing it. is_draft is the only mutable field, and only in the draft → committed direction — reopening a committed reconciliation back to draft is rejected. To undo a committed reconciliation, use Delete Reconciliation instead.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| id | integer | Reconciliation ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| is_draft | boolean | Yes | Must be false to approve the draft. No other fields are accepted |
Behavior
- If the reconciliation is already committed, the request is a no-op and returns the current state with
200 OK. - Otherwise, each cited journal entry line is re-checked to confirm it still has enough open amount to absorb the draft’s applied amount. If a cited line (or its journal entry) was cancelled or reconciled elsewhere since the draft was proposed, the stale draft is deleted and the request fails with
409 Conflict. - On success, the reconciliation is assigned a
sequence_number,is_draftbecomesfalse, and the cited journal entry lines and linked documents update their reconciled status — the same effect as Create Reconciliation.
Example Request
{ "is_draft": false }
Response
Returns the updated reconciliation with status 200 OK, including its entries.
Error Responses
| Status | Description |
|---|---|
| 400 | Missing/non-boolean is_draft, an unsupported field in the request body, or an attempt to move a committed reconciliation back to draft |
| 403 | No access to the reconciliation’s client account |
| 404 | Reconciliation not found |
| 409 | The reconciliation has already been deleted (is_deleted: true). Checked before staleness, so a deleted-and-stale draft returns this and not the staleness 409 below |
| 409 | The draft proposal is stale — a cited line no longer has enough open amount to approve it. The draft is deleted as part of this response; re-propose it if still needed |
Delete Reconciliation
DELETE /api/v2/reconciliations/{id}
Undoes a reconciliation. The journal entry lines and their journal entries remain in place — the reconciled amount is released.
- Draft reconciliations are proposals, not reconciled state — deleting one hard-deletes it.
- Committed reconciliations are soft-deleted instead of removed:
deleted_at/deleted_by_idare set andis_deletedbecomestrue, so the delete-and-redo is retained for audit and downstream signal. Soft-deleted reconciliations are hidden fromGET /reconciliationsunlessis_deleted=trueis requested.
Response
Returns 204 No Content with an empty body on success. Deleting an already soft-deleted reconciliation is idempotent — it returns 204 again rather than a 409 (unlike Update Reconciliation, which does 409 on an already-deleted draft).
Error Responses
| Status | Description |
|---|---|
| 403 | No access to the reconciliation’s client account |
| 404 | Reconciliation not found |
Notes
- The
client_account_idmust be one of the eligible client accounts for the authenticated user. - Reconciliations can only be created, updated, or deleted if they belong to one of the user’s eligible client accounts.
- The
entries,entries.journal_entry_line, andcreated_byrelations are opt-in onGET— request them throughwith.POSTandPATCHresponses includeentriesby default. - All amounts are returned as string representations of decimal numbers.
- All dates are returned in ISO 8601 format.
- The
_fcsuffix on amount fields represents “foreign currency” amounts. DELETEon a committed reconciliation is a soft delete (is_deleted: true,deleted_at/deleted_by_idset), not a removal — the row stays queryable viaGET /reconciliations?is_deleted=true.DELETEon a draft is a hard delete, since a draft is just an unapproved proposal.