Transactions
Syncing transactions for reconciliation
Last updated 14 September 2026
Reconciliation needs every transaction, exactly once, with enough detail to match it to an invoice, a payout run or a PSP settlement. This is the job that does that.
Which endpoint
Use GET /account/{xaid}/transactions per account rather than the global GET /transactions. Per-account paging keeps each run small, maps cleanly onto one ledger bank account, and means a problem on one account does not stall the others.
The fields that matter
| Field | Why |
|---|---|
| xtid | Unique transaction ID. Your dedupe key and your reference back to Xace. |
| credit | true for money in, false for money out. |
| amount, feeAmount, totalAmount | Amount, the fee attached to it, and the two combined. |
| currency | Always the account currency. |
| status, paymentStatus | Whether it has settled. Only post settled rows to the ledger. |
| paymentScheme | FPS, SEPA, SWIFT and so on. Useful for fee analysis. |
| reference, paymentReference, externalReference | What the counterparty wrote, what you wrote, and any reference you attached via the API. |
| counterpartName, counterpartIban, counterpartAccountNumber | Who the money came from or went to. |
| xpid | Set when the counterparty is a saved payee. Match on this first. |
| isFee, feeForXtid | See below. |
| transactionDate, postedDate | When it happened and when it was posted to the account. |
Fees are separate rows
Xace fees appear as their own transactions with isFee: true and feeForXtid pointing at the transaction they relate to. Post them to a fees account, linked to the parent by feeForXtid, rather than netting them into the payment. Your ledger will match bank statements and your fee reporting will be right.
The sync loop
async function syncAccount(xaid, token, since) {
const rows = await listAll(`/account/${xaid}/transactions`, token);
const fresh = rows.filter(t => new Date(t.postedDate) >= since);
for (const t of fresh) {
if (await ledger.has(t.xtid)) continue;
await ledger.insert({
id: t.xtid,
account: xaid,
direction: t.credit ? 'in' : 'out',
amount: t.amount,
fee: t.feeAmount,
currency: t.currency,
counterparty: t.counterpartName,
payee: t.xpid,
reference: t.reference,
scheme: t.paymentScheme,
postedAt: t.postedDate,
settled: t.status,
isFee: t.isFee,
feeFor: t.feeForXtid,
});
}
}Run it every 15 minutes with since set to the last run minus an hour. The overlap plus the xtid check means nothing is missed and nothing is doubled.
Matching
- If
xpidis set, match on the payee. - Otherwise match on
referenceorpaymentReferenceagainst open invoices and payout runs. - Otherwise match on
counterpartIbanorcounterpartAccountNumberagainst known counterparties, including your PSPs. - Anything left goes to an exceptions queue for a human.
Daily statements as the check
Use the CAMT.053 statement as the end-of-day control total. If the sum of your synced rows for the day does not equal the statement, something was missed. See [Automating daily statements](/guides/automating-daily-camt053-statements).