Posted Payments
Posted payments represent completed payment transactions in the system. They include details about the payment source and the actual transfer of funds.
A posted payment can also be a draft: a proposal (typically from Snapbooks automation for a bank transaction) that stores the transfer, account entries and reconciliation entries but has not been booked. A draft creates no journal entry, no reconciliation and no document status change, and its transfer stays at payment_status NONE until it is booked with PATCH /payments/{id}.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /payments | List posted payments |
| GET | /payments/{id} | Get a specific posted payment |
| POST | /payments | Create a posted payment (or a draft with "is_draft": true) |
| PATCH | /payments/{id} | Book a draft: body is exactly {"is_draft": false} |
| DELETE | /payments/{id} | Discard a draft (booked payments cannot be deleted; reverse their journal entry instead) |
Query Parameters
The GET /payments 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 |
| source_type | string | No | Filter by a single source type. The values Snapbooks sets are PAYMT (the payment posts a bank transaction) and PAYRQ (it books what a payment request paid). Comma-separated lists are not split — send one value |
| source_id | integer | No | Filter by source ID — the bank transaction or payment request ID, matching source_type |
| is_draft | boolean | No | true returns drafts only, false booked payments only; omitted returns both |
| with | string | No | Comma-separated nested relations to expand on the transfer. Supported: debtor.bank_account, debtor.business_partner, creditor.bank_account, creditor.business_partner. Note the paths have no transfer. prefix |
This endpoint does not paginate on request: it always answers with the
first 100 payments in the data/meta envelope, and page and
per_page are ignored. Narrow the result with source_type/source_id
instead — a payment of a bank transaction is found with
source_type=PAYMT&source_id=<transaction id>.
Create Posted Payment
POST /api/v2/payments
Creates a posted payment (or, with "is_draft": true, a draft proposal — see the note on drafts above).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account the payment belongs to |
| source_type | string | No | The type of source that generated this payment — PAYMT for a bank transaction, PAYRQ for a payment request |
| source_id | integer | No | The ID of the source |
| source_line_id | integer | No | The ID of the source line item |
| memo | string | No | Description or memo for the payment |
| is_draft | boolean | No | Create as an unbooked draft instead of posting immediately (default: false) |
| transfer | object | Yes | The transfer to create. Accepts the same fields as Payment Transfer, plus debtor.bank_account_id, creditor.bank_account_id, reconciliation_entries, and account_entries |
Any other field in the body is ignored rather than rejected.
Example Request
{
"client_account_id": 1,
"source_type": "PAYMT",
"source_id": 4821,
"memo": "Invoice payment",
"transfer": {
"payment_type": "CREDIT_TRANSFER",
"amount": "1000.00",
"currency_code": "EUR",
"requested_transfer_date": "2023-05-11",
"debtor": { "bank_account_id": 1 },
"creditor": { "bank_account_id": 2 },
"reconciliation_entries": [
{
"relation_type": "ARINV",
"relation_id": 123,
"amount": "1000.00",
"currency_code": "EUR"
}
]
}
}
Validation
Every payment — draft or booked — is checked for a consistent graph:
account_entriesare either all numbered withline_numberor none are, and the numbers must be unique.- When they are numbered, a
tax_for_linemust name a different entry’sline_number, and that entry must carry atax_code_id. The tax entry inherits that tax code and is markedtax_line.
Booking a payment of a bank transaction (source_type PAYMT) — a POST
without is_draft, or a PATCH that books a draft — is additionally checked
against the current books:
- The transfer amount must fit what is left to post of the bank transaction, so partial payments are allowed but overposting is not. A transaction that is already fully posted is rejected.
- Every document named by a
reconciliation_entriesentry must still be live and open. A deleted document, or one already settled, is rejected.
Payments with source_type PAYRQ are exempt from the last two checks —
they book what the payment request already paid.
Response
Returns the created posted payment with status 201 Created. See Attributes below.
Attributes
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the posted payment |
| client_account_id | integer | The ID of the client account |
| source_type | string | The type of source that generated this payment |
| source_id | integer | The ID of the source |
| source_line_id | integer | The ID of the source line item |
| memo | string | Description or memo for the payment |
| is_draft | boolean | true while the payment is an unbooked proposal |
| created_by_id | integer | The user who created the payment; 2 is Snapbooks automation |
| updated_by_id | integer | The user who last changed the payment, for example the one who booked a draft |
| human_touched_at | datetime | When a human last created or edited the payment; null if only automation has touched it |
| transfer | PaymentTransfer | The transfer details |
Payment Transfer
Each posted payment includes a transfer with the following attributes:
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the transfer |
| created_at | datetime | When the transfer was created |
| created_by_id | integer | The ID of the user who created the transfer |
| requested_transfer_date | date | The requested date for the transfer |
| payment_type | string | The type of transfer (CREDIT_TRANSFER, SALARY, TAX, INTERCOMPANY, MIXED) |
| payment_status | string | The current status of the transfer |
| rejection_code | string | Code indicating reason for rejection, if applicable |
| rejection_message | string | Detailed message about rejection, if applicable |
| booking_date | date | The date the transfer was booked |
| booking_reference | string | Reference number for the booking |
| amount | decimal | The transfer amount |
| currency_code | string | The currency code of the transfer |
| exchange_rate | decimal | The exchange rate used |
| creditor_reference | string | Reference for the creditor |
The transfer also always carries debtor and creditor objects, and the
reconciliation_entries and account_entries arrays below — none of them need
with.
Payment Reconciliation Entry
What the transfer settles: one entry per document (or document line) the
payment pays. transfer.reconciliation_entries is always present, empty when
the payment settles nothing.
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the entry |
| created_at | datetime | When the entry was created |
| created_by_id | integer | The ID of the user who created the entry |
| relation_type | string | Object type of what is settled, e.g. ARINV, APINV, ARCRN, APCRN |
| relation_id | integer | ID of the settled object |
| relation_line_id | integer | ID of the settled line on that object, when the payment settles a single line |
| amount | decimal | The amount applied to that object |
| currency_code | string | Currency of amount |
| payment_status | string | Status of this entry; see Payment Status Values |
| rejection_code | string | Code indicating why the entry was rejected, if applicable |
| rejection_message | string | Detailed rejection message, if applicable |
Payment Account Entry
How the transfer is posted: the general-ledger side of the payment, used when
the payment is not a plain settlement of a document (a fee withheld, an
interest charge, a tax line). transfer.account_entries is always present,
empty when the payment needs no extra ledger lines.
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the entry |
| created_at | datetime | When the entry was created |
| created_by_id | integer | The ID of the user who created the entry |
| transfer_id | integer | The transfer the entry belongs to |
| line_number | integer | Caller-assigned line number used by tax_for_line. Number every entry of a transfer or none |
| account_code | string | The GL account to post to |
| debit | decimal | Debit amount |
| credit | decimal | Credit amount |
| currency_code | string | Currency of debit/credit |
| tax_code_id | integer | The tax code applied to the entry |
| tax_base_amount | decimal | Net amount the tax is computed from |
| tax_amount | decimal | The tax amount |
| tax_line | boolean | true when the entry is the tax line of another entry |
| tax_for_line | integer | The line_number of the entry this tax line belongs to |
| memo | string | Description for the posting |
| dimensions | PaymentAccountEntryDimension | Dimensions to post the entry against |
Payment Account Entry Dimension
| Attribute | Type | Description |
|---|---|---|
| dimension_id | integer | The dimension to post against |
| relation_type | string | Object type of the dimension value: project, department, businesspartner |
| relation_id | integer | ID of the dimension value — the project, department or business partner |
Relationships
These are only present when requested with with (no transfer. prefix —
with=debtor.bank_account).
| Relationship | Type | Description |
|---|---|---|
| transfer.debtor.bank_account | BankAccount | The debtor’s bank account |
| transfer.debtor.business_partner | BusinessPartner | The business partner owning the debtor bank account |
| transfer.creditor.bank_account | BankAccount | The creditor’s bank account |
| transfer.creditor.business_partner | BusinessPartner | The business partner owning the creditor bank account |
Example Response
Requested with
?with=debtor.bank_account,debtor.business_partner,creditor.bank_account,creditor.business_partner
— without with, debtor and creditor hold only the two *_id fields.
{
"id": 1,
"client_account_id": 1,
"source_type": "PAYMT",
"source_id": 4821,
"source_line_id": null,
"memo": "Invoice payment",
"is_draft": false,
"created_by_id": 1,
"updated_by_id": null,
"human_touched_at": "2023-05-11T09:14:00+00:00",
"transfer": {
"id": 1,
"created_at": "2023-05-10T12:00:00+00:00",
"created_by_id": 1,
"requested_transfer_date": "2023-05-11",
"payment_type": "CREDIT_TRANSFER",
"payment_status": "COMPLETED",
"rejection_code": null,
"rejection_message": null,
"booking_date": "2023-05-11",
"booking_reference": "REF123",
"amount": 1000.0,
"currency_code": "EUR",
"exchange_rate": 1.0,
"creditor_reference": "INV123",
"debtor": {
"bank_account_id": 1,
"business_partner_id": 1,
"bank_account": {
"id": 1,
"account_number": "DE89370400440532013000"
},
"business_partner": {
"id": 1,
"name": "ACME Corp"
}
},
"creditor": {
"bank_account_id": 2,
"business_partner_id": 2,
"bank_account": {
"id": 2,
"account_number": "FR7630006000011234567890189"
},
"business_partner": {
"id": 2,
"name": "Supplier Inc"
}
},
"reconciliation_entries": [
{
"id": 11,
"created_at": "2023-05-10T12:00:00+00:00",
"created_by_id": 1,
"relation_type": "APINV",
"relation_id": 123,
"relation_line_id": null,
"amount": 1000.0,
"currency_code": "EUR",
"payment_status": "COMPLETED",
"rejection_code": null,
"rejection_message": null
}
],
"account_entries": []
}
}
Booking and discarding drafts
PATCH /payments/{id} accepts only {"is_draft": false}. Booking re-validates the proposal against the current ledger and then posts it exactly like a direct POST: the journal entry, reconciliation, external reconciliation and document status changes are created at that point, and created_by_id is kept, so a draft booked unchanged stays attributed to automation. Responses:
| Status | Meaning |
|---|---|
| 200 | Booked (or already booked; the call is idempotent) |
| 400 | Unsupported field, is_draft: true on a booked payment, or the proposal does not validate |
| 409 | The proposal is outdated (the bank transaction was settled another way, or a referenced document or bank account changed). The draft has been removed |
DELETE /payments/{id} removes a draft and returns 204. It returns 400 for a booked payment.
Undoing a booked payment
A booked payment cannot be deleted — reverse its journal entry instead (see
Journal Entries). Reversing it does more than remove the
postings: the payment’s transfer and every reconciliation entry move to
payment_status CANCELLED, and each document the payment settled is reopened
from what is still open of it, so the bank transaction and the documents can be
paid again.
Payments sourced from a payment request (source_type PAYRQ) are the
exception: the request, not the payment, settled those documents, so a reversal
leaves their status alone.
Notes
- The client_account_id must be one of the eligible client accounts for the authenticated user.
- When creating a posted payment, the created_by_id is automatically set to the authenticated user’s ID.
- The source_type and source_id fields track the origin of the payment — the bank transaction or payment request it posts.
- Bank account and business partner relations are expanded with the
withquery parameter (with=debtor.bank_account), notwith_relations. - Amounts are returned as JSON numbers, not strings. Send them as numbers or as decimal strings; both are accepted on write.
- Dates are ISO 8601 (
YYYY-MM-DD); timestamps are ISO 8601 with a UTC offset (2023-05-10T12:00:00+00:00). - The source_type filter accepts a single value; it is not a list parameter.