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. Today this is supported using a JSON data representation.
Upload statements as JSON
This guide covers how to upload statements in the JSON format
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
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",
"creditDebitType": "CREDIT",
"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",
"creditDebitType": "DEBIT",
"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.
The balance-to-transaction check
When you send transactions, they must account for the day in full:
sum(credits) − sum(debits) == 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 for the closing minus the opening booked balance, a credit if that is positive and a debit if it is negative, 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 | 1–100 |
| Transactions per statement | 1000 |
| Request body | 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. |
must_be_positive | statements.<n>.transactions.<i>.amount.value | The amount is negative — put the direction in creditDebitType. |
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 positive, with the direction increditDebitType - 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 about 3 hours ago
