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 file (file upload, template selector).

A wrong match is undone with bank/transactions/unmatch (Unmatch in Bank → Transactions). Posted entries are never edited or deleted: a payment matched to an invoice, or a line an import template posted to an account, gets a reversing journal transaction with every entry on the opposite side, dated date (default: today, and never before the original posting). The invoice's paid amount and payment status go back by the amount of that bank line. A line linked to a payment-provider settlement is only unlinked, because the settlement posting belongs to the batch. The line returns to status new and can be matched again. The reversal date must fall in an open accounting period.

Import templates

An import template names a data source, lists the fields the source delivers, and maps the fields that carry money to ledger accounts. Templates are managed under Bank → Import templates and via bank/import-templates/create|update|delete|get|list.

POST /v1/bank/import-templates/create
{
  "name": "Stripe payments export",
  "type": "stripe",
  "fields": [
    { "name": "id" },
    { "name": "Created date (UTC)" },
    { "name": "Amount" },
    { "name": "Fee", "accountCode": "6800" },
    { "name": "Amount Refunded" }
  ],
  "metaFields": ["order_id"]
}
  • type is one of stripe, iso20022, bank_connection or revolut.
  • Each field has a name and an optional accountCode. Only the fields that carry money accept an account: Stripe Amount, Fee and Amount Refunded; camt.053 Ntry/Amt; bank feed transaction_amount.amount. The account must exist in the chart of accounts (create it with ledger/accounts/create).
  • fields may be omitted; it then defaults to the standard field list of the type: the column names of the Stripe Payments → Export file (without the 19 Card … columns, which the form also leaves unticked by default), the camt.053 elements every statement carries (Stmt/Id, Stmt/Acct/Id/IBAN, Stmt/Bal/…, Ntry/Amt, Ntry/CdtDbtInd, Ntry/Sts, Ntry/BookgDt), or the attributes a PSD2 bank feed returns per transaction. Add or remove entries to match your file: the importer also reads the optional camt.053 elements Ntry/ValDt, Ntry/NtryRef, Ntry/AcctSvcrRef, Ntry/AddtlNtryInf, TxDtls/Refs/EndToEndId, TxDtls/Refs/TxId, TxDtls/RltdPties/Dbtr/Nm, TxDtls/RltdPties/DbtrAcct/Id/IBAN, TxDtls/RltdPties/Cdtr/Nm, TxDtls/RltdPties/CdtrAcct/Id/IBAN, TxDtls/RmtInf/Ustrd and TxDtls/AddtlTxInf.
  • A Stripe Customer Email field may carry "createPartner": true. The import then creates a customer (partner of type person, flagged as customer) for every email in the file that no existing partner has, using Customer Description as the name (the email when it is empty) and the Card Address … columns as the address when the file contains them. Existing partners are matched by email, case-insensitively. The response reports the count in customersCreated.
  • metaFields (Stripe only) lists the metadata keys your charges carry. Stripe exports each key as a <key> (metadata) column; the list is free-form and can be long.
  • invoiceItemId (Stripe only, optional) is a catalog item. When set, every invoice and credit note line the import creates references that item (its posting and, when invoiceVatRatePercent is empty, its VAT rate); the line text still comes from the charge.
  • companyMetaField (Stripe only) names one of those keys as the company flag. When a charge carries true, 1, yes, company, business or b2b in that column, the customer created for it is a partner of type company instead of person.
  • invoiceMetaField (Stripe only) names one of those keys, for example an order number, and invoiceVatRatePercent the VAT rate for invoices created from it. On import every charge and refund row is grouped by the value of that key:
    • a value that matches an existing sale invoice - a sales order whose externalRef is that value (ecommerce/orders/create, any channel) or an invoice whose fullNumber is that value - links the group to that invoice;
    • a value with no invoice creates one: partner from Customer Email (created when missing, as for createPartner), one line per charge (Checkout Line Item Summary or Description as the text, the charge amount as a VAT-inclusive price at invoiceVatRatePercent), issue date = the first charge date, VAT treatment from the company and the card country. The invoice is issued and posted at once, and a sales order with channel stripe and that value as externalRef is recorded so later imports link instead of creating;
    • each charge in the group is then matched to the invoice as a payment (posting the receipt); each refund creates an issued credit note against the invoice for the refunded amount and matches the refund to it. Counts come back as invoicesCreated, invoicesLinked, creditNotesCreated and paymentsMatched; groups that could not be processed (no customer email, mixed currencies, a linked invoice that is still a draft or already fully paid) are listed in warnings and left as new bank transactions. Do not also map Amount to a ledger account in the same template: a charge matched to an invoice is not posted a second time, but an unmatched one would be.
  • Template names are unique per company.
  • camt.053 and bank-connection templates carry the same options, read from the statement instead of Stripe columns:
    • Create missing partners: createPartner on the debtor name row (TxDtls/RltdPties/Dbtr/Nm, feed debtor.name) creates a customer for every unknown payer of incoming money; on the creditor name row (TxDtls/RltdPties/Cdtr/Nm, feed creditor.name) a supplier for every unknown payee of outgoing money. Partners are recognised by IBAN (stored in partner_bank_accounts), then by exact name; a new partner is a company when the name carries a legal form (UAB, MB, SIA, GmbH, Ltd, …), otherwise a person. The count comes back as customersCreated (file import) or partnersCreated (feed sync).
    • Link invoices by reference: invoiceMetaField set to a remittance or reference row (TxDtls/RmtInf/Ustrd, TxDtls/RmtInf/Strd/CdtrRefInf/Ref, TxDtls/Refs/EndToEndId; feed remittance_information, reference_number) matches every new bank line whose reference, remittance or description contains the number of an open issued sale invoice (money in) or registered purchase invoice (money out), and posts the payment. When the payer's IBAN is saved on a partner, or the payer's name equals a partner's name or code, the invoice must belong to that partner. A payment from an unknown payer is linked only when it equals the invoice's remaining balance and the invoice number carries its series prefix, so a bare number such as an amount is never taken as a reference. A payment larger than the remaining balance is never linked. Lines that fail these checks stay new for manual matching and are listed in the warnings. With advanceInvoices: true an unmatched incoming payment from a known partner creates an issued advance invoice (type advance) for the amount at invoiceVatRatePercent or the invoiceItemId rate and matches the payment to it; the partner must be known, so tick the debtor row too.
    • Posting rules by transaction code: besides Ntry/Amt / transaction_amount.amount, the field list has rule rows Ntry/BkTxCd=CHRG, =INTR, =TAXE, Ntry/CdtDbtInd=DBIT, =CRDT (feed: bank_transaction_code=…, credit_debit_indicator=…). A line is posted to the account of the first rule it matches: its bank transaction code family, then its direction, then the plain amount row. Rule rows are not checked against the file.
    • Bank charges partner: createPartner on the …=CHRG rule row creates a supplier named after the bank (statement Stmt/Acct/Svcr/FinInstnId/Nm, or the connection's bank, or the ledger bank account name) and books every charge with that partner.
    • Bank-connection templates are chosen per feed account with bank/feeds/accounts/configure (Bank → Connections in the app), which also sets syncSchedule (manual, daily, weekly, monthly): the API syncs accounts with a schedule by itself once their period has passed. Templates are applied on every bank/feeds/sync; the sync response then also reports posted, partnersCreated, invoicesLinked, invoicesCreated, paymentsMatched and warnings.

Pass templateId to bank/statements/import instead of format: the template's type selects the parser, and the file must contain every field listed in the template - a file missing any of them is rejected with HTTP 422 and the missing column or element names. Metadata fields are not required to be present. Bank-connection templates describe a live PSD2 feed and cannot be used for file import.

An ISO 20022 template also reads MT940 files: a file that starts with {1: or :20: (or is sent with "format": "mt940") is parsed as MT940 and the template's reference matching and posting rules are applied to its lines. The element check is skipped for MT940, and Ntry/BkTxCd=… rules never apply to MT940 lines because MT940 carries no ISO 20022 bank transaction code; use the Ntry/CdtDbtInd=… rules instead.

A bank account holds one currency. A statement in another currency is refused with HTTP 422 (in a camt.053 file with statements in several currencies for the same IBAN, only the statement in the account's currency is imported), and so is a line sent to bank/transactions/import with a currency other than the account's.

Every movement produced from a field that has an accountCode is posted at import time as one journal transaction: money in debits the bank account's ledger account and credits the mapped account; money out debits the mapped account and credits the bank account's ledger account. Amounts are converted to the base currency at the movement date. The bank transaction is then matched to that journal transaction (matchedDocumentType = journal_transaction) and no longer offered for invoice matching. The response reports the count in posted. Movements from unmapped fields stay new and are matched to invoices as before - for example map Fee to the PSP fee expense account and leave Amount unmapped so charges are still matched to sale invoices.

POST /v1/bank/statements/import
{
  "bankAccountId": "…",
  "templateId": "…",
  "content": "id,Created date (UTC),Amount,…"
}

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.

Revolut Business CSV

Download the transaction statement of a Revolut Business account as CSV and import it with the built-in template Revolut Business (templateId: "builtin-revolut"). Every company has this template without creating it: bank/import-templates/list-builtin lists it, bank/import-templates/get returns it, and bank/import-templates/update and bank/import-templates/delete refuse it with HTTP 400. In the app it is listed first under Bank → Import templates; opening it shows its settings, and saving creates a company template of your own that you can change.

POST /v1/bank/statements/import
{
  "bankAccountId": "…",
  "templateId": "builtin-revolut",
  "content": "Date started (UTC),Date completed (UTC),ID,Type,State,…"
}
  • Only rows with State = COMPLETED are imported. Other rows (for example PENDING) are left out and counted in warnings; they are imported by a later import of a file in which they are completed.
  • Each row becomes a bank transaction with the date from Date completed (UTC), the amount from Total amount, the currency from Payment currency, ID as externalId and Description as the description.
  • The counterparty of money out is Beneficiary name and Beneficiary IBAN; for a card payment without a beneficiary, the merchant in Description. The counterparty of money in is Sender name and Sender account.
  • Rows are grouped by account and currency. International account number must match the bank account's IBAN, and only rows in the bank account's currency are imported.
  • For every row the importer checks that the balance of the previous row plus Total amount equals Balance, and lists every row where it does not in warnings.
  • The file must contain the columns Date completed (UTC), ID, Type, State, Description, Payment currency, Total amount, Reference, Sender name and Beneficiary name.

On every import the built-in template:

  • creates a customer for every unknown sender of money in, and a supplier for every unknown beneficiary or card merchant of money out, as Create missing partners above describes;

  • matches every row whose Reference or Description contains the number of an open sale or purchase invoice, and posts the payment, with the checks of Link invoices by reference above;

  • posts the rows no invoice matched by their Type, to the account of a posting rule:

    Type Posting rule Default account Romanian chart Posting
    FEE bank.fees 6800 627 debit the rule's account, credit the bank account's ledger account; partner: a supplier named after the bank account
    CARD_PAYMENT bank.cardPayments 6209 628 debit the rule's account, credit the bank account's ledger account, without VAT
    TOPUP bank.topUps 3010 1012 debit the bank account's ledger account, credit the rule's account

    Rows of other types, such as TRANSFER, stay new for matching.

Change the accounts with ledger/posting-rules/update; the built-in template uses the new accounts from the next import. A posting rule cannot be turned off: to import card payments or top-ups without posting them, create a company template of type revolut without the Type=CARD_PAYMENT or Type=TOPUP row and import with it. A card payment posted this way is not matched to the supplier's invoice when that invoice is registered later; post one of the two, not both.

A company template of type revolut uses the same columns and can post rows by type with the rule rows Type=FEE, Type=CARD_PAYMENT, Type=TRANSFER and Type=TOPUP. Importing with "format": "revolut-csv" and no template imports the rows without any of these steps.

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).
  • A line whose charge, fee or refund was already imported onto another bank account from the Stripe payments export (bank/statements/import) is not booked to fees, receivables or suspense. Its net is credited to that bank account's ledger account instead, so the Stripe bank account clears payout by payout. summary.clearedAmount is the total moved this way; a line whose amount differs from what that account holds is named in warnings. If nothing is left to book, the batch is posted with journalTransactionId null.

bank/settlements/list and /get return, per batch, clearedNet, clearingDifference (payout lines minus the amounts the other bank account holds for them; zero when they agree) and clearingOpenCount (lines with a difference or an unposted movement); /get returns the same per line as clearingBankAccountId, clearingBooked, clearingDifference and clearingUnposted. Refund lines of one charge are compared together, because the payments export holds only the refunded total. A Stripe payments export imported after its payout was posted skips the movements of that payout and names it in warnings.

VAT on the commission itself is invoiced separately (issue a commission invoice to the seller); the split only allocates the collected cash.

Payouts in another currency

The journal is always written in the company base currency, but a payout in a different currency posts automatically - no manual entry is needed:

  • The batch converts at the rate for the posting date (date, falling back to the payout date, then today). The rate is the company's own rate for that currency (reference/exchange-rates/set) when it is at least as recent as the official one, otherwise the latest official rate on or before that date. With no rate on or before the date the call fails with 422 and names reference/exchange-rates/sync.
  • Every account converts at that batch rate except accounts receivable, which is relieved at the fxRate each invoice was issued at. The residual between the two rates posts to bank.fxGain (default 5803) or bank.fxLoss (default 6803) as a realized exchange difference, so the journal still balances.
  • The response summary returns the fxRate used and the resulting exchangeDifference, and the batch keeps the rate it was posted at. A matched invoice whose own currency differs from the batch currency is not settled: its amount goes to the suspense account with a warning.
  • One import limit remains: a single payout whose rows mix several currencies is rejected with 422, as is a re-import whose currency differs from the batch already stored for that payout.

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.