Skip to content

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

FieldWhy
xtidUnique transaction ID. Your dedupe key and your reference back to Xace.
credittrue for money in, false for money out.
amount, feeAmount, totalAmountAmount, the fee attached to it, and the two combined.
currencyAlways the account currency.
status, paymentStatusWhether it has settled. Only post settled rows to the ledger.
paymentSchemeFPS, SEPA, SWIFT and so on. Useful for fee analysis.
reference, paymentReference, externalReferenceWhat the counterparty wrote, what you wrote, and any reference you attached via the API.
counterpartName, counterpartIban, counterpartAccountNumberWho the money came from or went to.
xpidSet when the counterparty is a saved payee. Match on this first.
isFee, feeForXtidSee below.
transactionDate, postedDateWhen 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

sync-transactions.jsjavascript
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

  1. If xpid is set, match on the payee.
  2. Otherwise match on reference or paymentReference against open invoices and payout runs.
  3. Otherwise match on counterpartIban or counterpartAccountNumber against known counterparties, including your PSPs.
  4. 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).

Was this page helpful?
Suggest edits