Uploading statements (bring your own data)
Bring your own data
Atlar normally fetches your account data from the bank itself. When that is not possible, e.g. the bank has no channel Atlar can connect to, the data sits in a PSP or an ERP, or you already hold the files yourself, you can send the data to Atlar instead. This is what we call bring your own data (BYOD).
There is more than one way to do it, and which one fits depends on how much data you have and how it reaches you:
- Enter a balance in the Atlar app. For an account you only need to keep roughly up to date, type the closing balance for a day directly in the dashboard. No integration work, but no transactions either.
- Upload a bank file. If your bank sends you camt.053, MT940 or OFX files, post them to Atlar as they are. Atlar parses them, so nothing has to be transformed on your side.
- Submit a report in Atlar's own format. When the data comes from a system that doesn't produce bank files, a PSP, an ERP, your own ledger, describe the account's end-of-day position in Atlar's statement format instead. You can send it as JSON or as a CSV file.
Upload statements in Atlar's formats
This guide covers how to upload statements as JSON or as CSV. Both describe the same statements, and the rules below apply to both, unless a section says otherwise.
Before you start
- A BYOD connection. Atlar sets this up for you. It is a connection which is configured to not fetch data from a third party.
- API credentials with the
connections:readandconnection_reports:createpermissions. See Accessing the API. - The connection ID of that BYOD connection, referred to as
{cid}below.
The model
report
└── statement one account, one local date
├── balances where the account stood, start and end of day
└── transactions the individual movements that got it there
- A report is one upload. It is the audit record of what you sent, and it can carry statements for many accounts at once.
- A statement is one account's settled position for one local calendar date. It is identified by the account plus that date, and every date inside it, the period, every balance, every transaction, must be that same date.
- Balances say where the account stood. Atlar reconciles against the booked balances, so every statement must carry a booked opening and a booked closing balance.
- Transactions say what moved. They are optional, but without them a statement only shows that the balance changed, not what changed it — and nothing can be matched against a payment.
Two properties follow from this model and are worth internalising before you build anything:
- Everything is a local date, never UTC. You send
2024-09-26, and Atlar turns it into the exact instants the account's business day starts and ends, using the account's own timezone. A wrong timezone moves the whole statement onto the neighbouring day. - An account and a date can be recorded only once. A second statement for a date that already has one does not correct it. Get the figures right before you upload.
Uploading as JSON
POST https://api.atlar.com/connectivity/v2beta/connections/{cid}/reports
Content-Type: application/json
curl -X POST "https://api.atlar.com/connectivity/v2beta/connections/$CID/reports" \
-u "$ATLAR_ACCESS_KEY:$ATLAR_ACCESS_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"statements": [
{
"type": "BOOKED",
"period": { "type": "EODY", "localDate": "2024-09-26" },
"account": {
"market": "DE",
"currency": "EUR",
"timezone": "Europe/Berlin",
"identifiers": [
{ "type": "IBAN", "market": "DE", "number": "DE29700700240109999999" }
],
"routing": [ { "type": "BIC", "number": "DEUTDEBBXXX" } ]
},
"holder": { "legalName": "Acme GmbH", "partyType": "COMPANY" },
"balances": [
{
"type": "BOOKED", "reportedType": "OPEN", "localDate": "2024-09-26",
"amount": { "currency": "EUR", "stringValue": "5000.00" }
},
{
"type": "BOOKED", "reportedType": "CLOSE", "localDate": "2024-09-26",
"amount": { "currency": "EUR", "stringValue": "10000.00" }
}
],
"transactions": [
{
"uniqueID": "acme-2024-09-26-0001",
"status": "BOOKED",
"bookedDate": "2024-09-26",
"valueDate": "2024-09-27",
"description": "Invoice 1042",
"amount": { "currency": "EUR", "stringValue": "6000.00" },
"references": { "endToEndId": "E2E-1042" },
"counterparty": {
"holder": { "legalName": "Beispiel AG", "partyType": "COMPANY" },
"account": {
"market": "DE",
"currency": "EUR",
"rawIdentifierNumber": "DE02120300000000202051"
}
}
},
{
"uniqueID": "acme-2024-09-26-0002",
"status": "BOOKED",
"bookedDate": "2024-09-26",
"description": "Office rent",
"amount": { "currency": "EUR", "stringValue": "-1000.00" }
}
]
}
]
}'Note that the two transactions net to +5000.00, exactly the difference between the opening and the closing booked balance. That is a hard requirement, see The balance-to-transaction check.
A 201 Created returns the report resource:
{
"id": "0191f3c4-8f2a-7d31-9b6e-3a4c5d6e7f80",
"connectionId": "…",
"organizationId": "…",
"details": { "type": "STATEMENT", "statementType": "BOOKED" },
"content": { "contentType": "application/vnd.atlar.connectivity.v2.statement+json", "…": "…" },
"created": "2024-09-27T06:12:44.912Z",
"version": 1
}The 201 means Atlar has accepted and stored your upload and everything it validates up front passed. Processing happens asynchronously, see Confirming the data landed.
Uploading as CSV
If your data is easier to export as a spreadsheet, upload it as a CSV file instead. Each row is one transaction. Atlar groups the rows into statements and works out the opening and closing balances from a running balance column.
Post the file to the same endpoint as a multipart/form-data request. Put the CSV in the file field (one file per request), and set fileContentType to application/vnd.atlar.connectivity.v2.statement+csv:
curl -X POST "https://api.atlar.com/connectivity/v2beta/connections/$CID/reports" \
-u "$ATLAR_ACCESS_KEY:$ATLAR_ACCESS_SECRET" \
-F "fileContentType=application/vnd.atlar.connectivity.v2.statement+csv" \
-F "[email protected]"This statements.csv holds the same day as the JSON example above, plus the following day:
type,period.localDate,holder.legalName,account.market,account.currency,account.type,account.bic,account.identifiers.0.type,account.identifiers.0.market,account.identifiers.0.number,transaction.uniqueId,transaction.amount.stringValue,transaction.description,transaction.runningBalance.stringValue
BOOKED,2024-09-26,Acme GmbH,DE,EUR,CHECKING,DEUTDEBBXXX,IBAN,DE,DE29700700240109999999,acme-2024-09-26-0001,6000.00,Invoice 1042,11000.00
BOOKED,2024-09-26,Acme GmbH,DE,EUR,CHECKING,DEUTDEBBXXX,IBAN,DE,DE29700700240109999999,acme-2024-09-26-0002,-1000.00,Office rent,10000.00
BOOKED,2024-09-27,Acme GmbH,DE,EUR,CHECKING,DEUTDEBBXXX,IBAN,DE,DE29700700240109999999,acme-2024-09-27-0001,-250.00,Card fees,9750.00It becomes two statements: 2024-09-26, opening at 5000.00 and closing at 10000.00, and 2024-09-27, opening at 10000.00 and closing at 9750.00. The response is the same 201 Created report resource as for JSON.
How rows become statements
- Rows with the same account identifier (
account.identifiers.0.*),account.currencyandperiod.localDateform one statement. One file can hold many accounts and many days. - The account and holder columns must be identical on every row of a statement.
- Every transaction is booked on its statement's
period.localDate.
Amounts and the running balance
transaction.amount.stringValueis signed from the account's point of view: positive for money in, negative for money out. Write it with exactly as many decimals as the currency uses, e.g.100.00for EUR, not100.0.transaction.runningBalance.stringValueis the account's balance right after that transaction.- Within a statement, the rows are read in file order. Each row's running balance must equal the previous row's running balance plus its own amount.
- The opening balance is the first row's running balance minus its amount. The closing balance is the last row's running balance.
Because the balances are derived from the transactions, a CSV upload always passes the balance-to-transaction check. Make sure each day's opening balance still matches the previous day's closing balance.
Columns
Column order does not matter. Columns can be separated by commas, semicolons or tabs, and a UTF-8 byte order mark (as Excel adds when saving as "CSV UTF-8") is fine. A column that Atlar does not recognise is rejected.
Required columns:
| Column | Content |
|---|---|
type | BOOKED |
period.localDate | The statement's local date, YYYY-MM-DD |
holder.legalName | The account holder's legal name |
account.market | Two-letter country code, e.g. DE |
account.currency | Three-letter currency code, e.g. EUR |
account.type | e.g. CHECKING |
account.bic | The account's BIC |
account.identifiers.0.type | e.g. IBAN |
account.identifiers.0.market | Two-letter country code of the identifier |
account.identifiers.0.number | The identifier itself, e.g. the IBAN |
transaction.uniqueId | 10–256 characters, see Uniqueness and re-uploads |
transaction.amount.stringValue | Signed amount |
transaction.description | The description as reported by the source system |
transaction.runningBalance.stringValue | The balance after this transaction |
Optional columns:
- Account:
account.name, and a second identifier asaccount.identifiers.1.type,account.identifiers.1.market,account.identifiers.1.number. - Holder:
holder.partyType,holder.email,holder.address.country,holder.address.countrySubdivision,holder.address.city,holder.address.postalCode,holder.address.streetName,holder.address.streetNumber,holder.nationalIdentifier.type,holder.nationalIdentifier.market,holder.nationalIdentifier.number. - Transaction:
transaction.valueDate,transaction.instructedAmount.currency,transaction.instructedAmount.stringValue. - Counterparty:
transaction.counterparty.account.rawIdentifierNumber,transaction.counterparty.account.market,transaction.counterparty.account.currency,transaction.counterparty.account.bic,transaction.counterparty.holder.legalName,transaction.counterparty.holder.partyType,transaction.counterparty.holder.nationalIdentifier.type,transaction.counterparty.holder.nationalIdentifier.market,transaction.counterparty.holder.nationalIdentifier.number, and the sameaddress.*columns as the holder undertransaction.counterparty.holder.address.. - References: one column per reference, named
transaction.references.<name>, e.g.transaction.references.endToEndId.
There is no timezone column. Atlar resolves the account's timezone from account.market and account.bic.
Balances only
If you only have end-of-day balances, leave out the transaction columns and send one row per account and day. Atlar uses this layout whenever the header has no transaction.runningBalance.stringValue column. Each row becomes a statement without transactions, handled as described in Omitting transactions.
type,period.localDate,holder.legalName,account.market,account.currency,account.type,account.identifier.type,account.identifier.number,account.routing.type,account.routing.number,balances.OPEN.amount.currency,balances.OPEN.amount.stringValue,balances.CLOSE.amount.currency,balances.CLOSE.amount.stringValue
BOOKED,2024-09-26,Acme GmbH,DE,EUR,CHECKING,IBAN,DE29700700240109999999,BIC,DEUTDEFFXXX,EUR,5000.00,EUR,10000.00
BOOKED,2024-09-27,Acme GmbH,DE,EUR,CHECKING,IBAN,DE29700700240109999999,BIC,DEUTDEFFXXX,EUR,10000.00,EUR,9750.00CSV errors
Errors have the same shape as for JSON, but field points into the file. Rows are counted from 1, starting with the first row after the header:
| Field | Meaning |
|---|---|
file.0.<column> | A problem with the header, e.g. a required column is missing. |
file.<s>.<r>.<column> | Row r of the statement that starts on row s, e.g. file.1.2.transaction.runningBalance.stringValue. |
file.<r>.<column> | Row r of a balances-only file. |
CSV-specific codes:
| Code | Meaning |
|---|---|
running_balance_mismatch | The running balance is not the previous row's running balance plus this amount. |
inconsistent_value | An account or holder column differs between rows of the same statement. |
invalid_amount | The amount cannot be read, e.g. it has the wrong number of decimals. |
The balance-to-transaction check
When you send transactions, they must account for the day in full:
sum(transactions[].amount) == closing booked balance − opening booked balance
exactly, to the minor unit. A request that does not balance is rejected with 400 and the error code sum_mismatch on statements.<n>.transactions.
This is the single most common reason an upload is refused, and it is deliberate. If Atlar accepted balances and transactions that disagreed, your reported balance and the movements explaining it would permanently contradict each other, and no amount of reconciliation later could tell which of the two was right.
Practical consequences:
- Do not filter. Fees, interest, charges, internal transfers, reversals. If it hit the account that day, it belongs in the array.
- Charges deducted from a payment must be sent as their own movement, or as an
amountthat reflects what actually hit the account with the original ininstructedAmount. Either way the sum must come out right. - If you cannot make it balance, do not paper over it with a plug entry. Fix the source extraction, or omit the transactions array entirely for that day and send balances only.
Omitting transactions
If you leave the array out, Atlar reconstructs the day's net movement from the balances: a single transaction whose amount is the closing minus the opening booked balance (positive if the balance rose, negative if it fell), dated on the statement's local date. If the two balances are equal, the day gets no transaction at all.
That keeps your balances correct, but the day's activity stays opaque and cannot be reconciled against individual payments. Treat it as a fallback, not a design.
Uniqueness and re-uploads
uniqueID is how Atlar recognises a movement it has already seen. Its identity is the uniqueID together with the amount and the booked date, per account.
| What you do | What happens |
|---|---|
| Re-send the same transaction unchanged | Ignored. The stored movement is left untouched. This is not an update. |
Re-send the same uniqueID with a different amount or booked date | A conflict. It fails during processing and needs Atlar to intervene. |
| Re-send an entire statement, figures unchanged | Accepted, then discarded during processing. The first upload stands. |
| Re-send a statement for the same account and date with different figures | Accepted, then fails during processing. The stored statement is not replaced or corrected. Resolving it requires Atlar to intervene. |
| Upload the same JSON body twice | Two report resources, one per request. Deduplication happens at the statement and transaction level, not at the report level, so a retry is safe but does not overwrite anything. |
So: a uniqueID must be stable across re-uploads and must never be reused for a different movement. If your source system has its own transaction identifier, use it. If it does not, derive one deterministically from fields that cannot change — never from a row number, a position in a file, or a timestamp of when you happened to run the export.
Within a single upload, Atlar rejects a uniqueID that appears twice for the same account and booked date, whether in the same statement (not_unique on the transaction's uniqueID) or across two statements in the same report.
The safe practice is to make your uniqueID values unique across your whole system, not merely within one file.
Limits
| Limit | Value |
|---|---|
| Statements per request (JSON) | 1–100 |
| Transactions per statement | 1000 |
| Request body (JSON) | 10 MiB |
uniqueID length | 10–256 characters |
references entries per transaction | 32 |
Account identifiers | 1–10 |
Account routing | 10 |
A day with more than 1000 movements on one account cannot be split across two statements, because an account and date can be recorded only once. Contact Atlar if you have such an account.
For a backfill, upload in chronological order and one statement per account and date, with no gaps in the date range. A missing day leaves a hole in the account's balance history that later uploads do not fill.
Errors
A rejected request returns 400 with a details array. Each entry names the offending field by its path in your request body and a machine-readable code:
{
"message": "Bad request",
"details": [
{
"field": "statements.0.transactions",
"code": "sum_mismatch",
"message": "the sum of the transactions must equal the closing booked balance minus the opening booked balance"
}
]
}Common codes:
| Code | Field | Meaning |
|---|---|---|
sum_mismatch | statements.<n>.transactions | The transactions do not add up to the balance movement. |
not_unique | statements.<n>.transactions.<i>.uniqueID | The uniqueID is already used in this upload for the same account and date. |
invalid_date | statements.<n>.transactions.<i>.bookedDate | The booked date is not the statement's date. |
currency_mismatch | …amount.currency | A transaction or balance is not in account.currency. |
required | statements.<n>.balances | Transactions were supplied without a booked opening and closing balance. |
unsupported_content_type | file | The connection is not configured to accept this format. |
If one record in the upload is invalid, nothing is created. There are no partially successful reports: fix the reported records and resend the whole request.
Another common error code is 412 Precondition Failed which indicates that the connection fetches its reports from a bank, so it does not accept uploads. Use your BYOD connection.
Failures after the 201
201Some conditions can only be established once Atlar processes the upload, so the request is accepted and the statement is then rejected downstream. The main ones:
- the account's timezone could not be resolved from
timezone,market, or aBIC - a booked opening and closing balance pair is missing (when you sent no transactions)
- a balance's
localDateis not the statement's date - a balance is not in the account's currency (when you sent no transactions)
- a conflict with a statement or a transaction already recorded for that account and date
These need Atlar to intervene, which is why the checklist below is worth running before you go live.
Confirming the data landed
GET /connectivity/v2beta/connections/{cid}/reports/{id}— the report you created.GET /connectivity/v2beta/connections/{cid}/reports/{id}/bank-statements— the statements Atlar derived from it. Statements appearing here means conversion succeeded.- The account's balances and transactions through the Financial Data API, or in the Atlar app. This is the real confirmation: the figures you sent are visible on the account and available to reconcile.
Give processing a moment before checking. Processing is asynchronous and does not block your 201.
Checklist
Before your first production upload, confirm for a sample day that:
- every date in the statement is the account's local date, and
account.timezoneis set -
period.localDate, everybalances[].localDate, and everytransactions[].bookedDateare the same date - there is exactly one
BOOKED/OPENand oneBOOKED/CLOSEbalance - yesterday's closing balance equals today's opening balance
- all transaction and balance currencies equal
account.currency - every transaction
amountis signed from the account's point of view: positive for money in, negative for money out - the transactions net exactly to closing minus opening
- every
uniqueIDcomes from a stable source identifier, not a row number - the account identifiers are what the bank reports, IBAN first
Updated 3 days ago
