Nordlet

Docs / Guides

Bank & payment imports

camt.053 statements, Stripe CSV, PSP settlement reconciliation and custom JSON feeds.

All imports land as bank transactions on a bank account (bank/accounts/create). Transactions carry an externalId; re-importing the same file is safe — rows whose (bankAccountId, externalId) already exist are skipped and reported in the skipped counter. Imported transactions start in status new and are posted to the ledger only when matched (bank/transactions/match, with bank/transactions/suggest-matches for candidates).

The web app exposes the same imports under Bank → Import statement (file upload, format selector).

ISO 20022 camt.053 (open banking XML)

Lithuanian banks (Swedbank, SEB, Luminor, Šiaulių bankas, …) let you download account statements as ISO 20022 camt.053 XML from their internet banking — usually under statement export/download, format "ISO XML" or "camt.053". The same format is returned by PSD2 account-information APIs.

POST /v1/bank/statements/import
{
  "bankAccountId": "…",
  "format": "camt053",
  "content": "<?xml version=\"1.0\"…>…"
}
  • A file may contain several statements; each statement's IBAN must match the bank account's IBAN (statements without an IBAN, or accounts without one, are accepted as-is).
  • Entry references (AcctSvcrRef/NtryRef, falling back to a content hash) become externalId.
  • The response echoes per-statement metadata: statement id, period, opening/closing balances, transaction count.
  • Counterparty name, IBAN and payment purpose are extracted per entry for matching.

Payments in the other direction are exported as pain.001 via bank/payments/export.

Stripe CSV

Export from the Stripe dashboard — either report works:

  • Reports → Balance → Itemized balance change from activity (recommended): columns balance_transaction_id, created_utc, currency, gross, fee, net, reporting_category, description.
  • Payments → Export: columns id, Created (UTC), Amount, Fee, Currency, Description, Customer Description.
POST /v1/bank/statements/import
{
  "bankAccountId": "…",
  "format": "stripe-csv",
  "content": "balance_transaction_id,created_utc,currency,gross,fee,net,…"
}

Create a dedicated bank account for Stripe (e.g. name "Stripe", GL account 2710 or its own subaccount) — the Stripe balance behaves like a bank account: charges flow in, fees are deducted, payouts transfer the balance to your real bank.

Import behaviour:

  • Each balance transaction becomes one bank transaction for the gross amount (externalId = balance transaction id).
  • A non-zero fee becomes a second transaction with the opposite sign (externalId = <id>:fee, counterparty "Stripe") — so fees can be matched/posted as expenses separately from revenue. Fees returned on refunds come in as positive amounts.
  • Payout rows import as single negative transactions; match them against the corresponding incoming transaction on your real bank account statement.
  • Zero-amount rows are skipped; the date is taken from created_utc (UTC).
  • Payments-export rows that moved no money are skipped: Captured = false (authorizations/delayed captures) and Status Failed/Canceled.
  • A non-zero Amount Refunded becomes a third, negative transaction (externalId = <id>:refund) dated Refunded date (UTC) — so partial and full refunds embedded in payment rows are not lost.

PSP settlement reconciliation (Stripe payout reports)

For splitting a lump-sum payout into its underlying orders, refunds, fees and chargebacks, use the settlement endpoints instead of the plain statement import. Two Stripe exports are accepted (detected automatically):

  • Reports → Payouts → Payout reconciliation, itemized (report types payout_reconciliation.itemized.* or payout_reconciliation.by_id.itemized.*) — exact, one row per balance transaction; preferred on live accounts.
  • Payments → Export (unified payments) — the charge-level export; works in test/sandbox mode where payout reconciliation is unavailable. Captured charges are grouped by the payout id in the Transfer column; uncaptured/failed rows are skipped (skippedNotSettled); a non-zero Amount Refunded becomes a synthetic refund line in the charge's payout batch — an approximation, since the refund may really have settled in a later payout. Match references come from the charge id, PaymentIntent ID, Invoice Number, Client Reference ID and every … (metadata) column (e.g. an order number in metadata).

One CSV can span many payouts. Stripe downloads the itemized report one file per section (…_charge_…, …_refund_…, …_fee_…); import them all — rows for an already-imported payout merge into the existing batch (reported in the updated counter) as long as it has not been posted yet, and batch totals are recomputed.

POST /v1/bank/settlements/import
{
  "bankAccountId": "…",
  "provider": "stripe",
  "content": "balance_transaction_id,created_utc,…,reporting_category,…,automatic_payout_id,…"
}

Import behaviour:

  • Rows are grouped by automatic_payout_id into settlement batches — one batch per payout, with gross/fee/net totals (sum of row net = the amount that hits your bank). Re-importing a payout is skipped (skipped counter). Rows not yet assigned to a payout and the payout rows themselves are skipped and counted separately.
  • Every charge/refund/dispute row is auto-matched to a sale invoice using, in order: an e-commerce order's externalRef (any value from order_id, payment_intent_id, source_id, charge_id or any *_metadata[...] column), an invoice fullNumber appearing in those columns, and — for refunds and chargebacks — the charge_id of a previously matched charge (same file or earlier imports).
  • Review with bank/settlements/list / bank/settlements/get; fix leftovers with bank/settlements/match { lineId, invoiceId } (or invoiceId: null to unmatch).

POST /v1/bank/settlements/post { id, date?, commissionPercent? } posts the whole batch as one balanced journal transaction and settles the matched invoices (paidAmount/paymentStatus, sale_invoice.paid webhooks):

  • Debit the bank account GL for the net payout; debit PSP fees (settlements.fees, default 6800) for withheld fees and fee rows.
  • Credit accounts receivable per matched charge; refunds and chargebacks reverse it. Application is capped at each invoice's remaining amount — any excess lands on the suspense account with a warning.
  • Unmatched charges: with commissionPercent set they are split into commission revenue (settlements.commissionRevenue, default 5001) and a seller liability (settlements.sellerPayable, default 4499) — the marketplace flow where collected money mostly belongs to sellers. Without it they are parked on settlements.suspense (default 4440, advances received).
  • platform_earning rows (Stripe Connect application fees) post to commission revenue; transfer rows (payouts to connected sellers) reduce the seller liability.
  • All four accounts are overridable per company via posting rules (ledger/posting-rules).

VAT on the commission itself is invoiced separately (issue a commission invoice to the seller); the split only allocates the collected cash. Settlements are posted in the company base currency — foreign-currency payouts must currently be posted manually.

Manual / custom feeds

Anything else can be pushed as plain JSON:

POST /v1/bank/transactions/import
{
  "bankAccountId": "…",
  "transactions": [
    { "date": "2026-07-01", "amount": "-25.00", "currency": "EUR",
      "counterpartyName": "…", "description": "…", "externalId": "unique-ref-1" }
  ]
}

Provide a stable externalId per transaction to keep re-imports idempotent.