> For the complete documentation index, see [llms.txt](https://developer.vario-software.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.vario-software.de/documentation/apps/app-backends/banking.md).

# Banking

Deliver account transactions into the ERP and send payments out.

A banking app delivers **account transactions** into the ERP and optionally sends **payments** out. PayPal and file-based banking (CAMT/PAIN) are the reference implementations.

{% hint style="info" %}
Read [App Backends](/documentation/apps/app-backends.md) first; this page only covers what is specific to banking.
{% endhint %}

## Registering the bank backend

```javascript
const bankBackend = await methods.createBankBackend('PayPal', 'APP');

await methods.changeBankBackend(bankBackend.id, {
  ...bankBackend,
  description: 'Manages PayPal transactions, refunds and open-item matching.',
});
```

Two calls, because `createBankBackend` has no description parameter. `changeBankBackend` is a **full PUT** — spread the created object so `label` and `version` survive.

{% hint style="info" %}
`backendType` is `APP` for apps. Other values exist for built-in providers.
{% endhint %}

### You register a backend — the customer creates the bank

This is the key decision when you start:

> Registering the backend makes your app **selectable** in the ERP's "create bank" dialog. The customer then creates their own bank — with the correct name, IBAN and BIC — and picks your backend.

Do **not** create banks on install. The customer owns that master data. The one exception is a provider where the "bank" is the provider itself and there is nothing for the customer to name — the PayPal app creates a single `PayPal` bank, one **account per connected merchant**.

### Letting the ERP call you

The bank backend record carries three URL fields. Set them if the ERP should drive the interaction:

| Field               | Purpose                        |
| ------------------- | ------------------------------ |
| `appTransactionUrl` | Fetch transactions             |
| `appPaymentUrl`     | Send a payment or direct debit |
| `appStatusUrl`      | Poll the status of a payment   |

If your app pulls on a schedule instead, you don't need them — use a [cron webhook](https://developer.vario-software.de/documentation/webhooks/introduction).

## Delivering transactions

One POST per transaction. There is no bulk endpoint:

```javascript
await app.erp.fetch('/erp/bank/transactions', { 
  method: 'POST', 
  body: transaction, 
});
```

The fields that matter:

| Field                                      | Notes                                                       |
| ------------------------------------------ | ----------------------------------------------------------- |
| `accountId`                                | The ERP bank account this belongs to                        |
| `bookingDate`, `valueDate`                 | Date-time                                                   |
| `amount`                                   | **Always positive** — the sign lives in `direction`         |
| `direction`                                | `INCOMING` or `OUTGOING`                                    |
| `currency`                                 | ISO code                                                    |
| `endToEndId`                               | Your stable identifier — this is what deduplication keys on |
| `purpose`, `name`, `iban`, `accountNumber` | Counterparty and reference data                             |

`matchingState` and `isPotentialDuplicate` are set by the ERP — don't try to write them.

Balances are updated separately, and the value goes in the **query string**:

```
PUT /erp/bank/accounts/{id}/balance?balance=1234.56
```

## Matching against open items

You do not implement matching. Hand it to the ERP after importing:

```javascript
await app.erp.fetch('/erp/finance/openitemsynchronization', { 
  method: 'POST', 
  body: { bankId, bankAccountId }, 
});
```

Call it once per account **after** a batch, not per transaction. Only call it when something was actually imported.

## Sending payments

Outbound is the mirror image: read payments the ERP has prepared, send them, then report back.

* Only payments with status `CREATED` or `PREPARED` are pending.
* Credit transfers are `MONEY_TRANSFER` / `REALTIME_MONEY_TRANSFER`; direct debits are `SEPA_CORE_DIRECT_DEBIT` / `SEPA_B2B_DIRECT_DEBIT`.
* Confirm with `POST /erp/bank/payments/{id}/update_status/SENT` — and only after the bank or provider has actually accepted it.

{% hint style="warning" %}
**Archive before you mark as sent.** If you write the file to the DMS *after* transitioning payments to `SENT`, a failed upload leaves payments marked as sent with no record of what the bank received. Upload first, then transition.
{% endhint %}

## What to watch out for

**Deduplication is your responsibility.** The ERP flags potential duplicates but does not reject them. Key on `endToEndId`. Two traps:

* If you only check for existing ids **within the date window you just queried**, a transaction whose booking date falls outside that window will be imported again. Overlapping sync windows make this likely.
* Transactions **without** an `endToEndId` can never be deduplicated. Derive a stable identifier for them or accept duplicates.

For file-based imports, layer the checks: hash of the whole file, then statement identity, then per-entry id. A file that failed to import must still be retryable — treat an import as "already done" only if it actually created rows.

**Provider data lags.** Reporting APIs often do not have the last few hours. Ending a sync window at "now" returns empty results or an error. End it deliberately in the past and track how far you have synced.

**Windows have maximum lengths.** Many providers cap a query at around a month. Split a long back-fill into chunks rather than requesting the whole range.

**Watch the sync cursor.** If a manual sync with an explicit date range writes the same "synced to" value the scheduled job reads, a user investigating an old transaction can move the cursor backwards or forwards and skip days. Keep the cursor separate from ad-hoc queries.

**Dates as strings.** If you compare dates lexicographically, never mix date-only (`2026-01-15`) and date-time (`2026-01-15T10:30:00Z`) values in the same field — the comparison silently breaks.

**Amounts.** Send the absolute value and set `direction`. Losing the direction flag silently inverts a booking. Keep two decimals and be careful with floating-point sums over large batches.

**SEPA is EUR-only** and its character set is restricted — transliterate umlauts and accents, and strip anything outside the permitted set before generating a file, or the bank rejects it. The SEPA creditor identifier (Gläubiger-ID) lives on the ERP **bank** record, not in your app configuration.

**Credentials per connected account.** A tenant may connect several merchants or accounts to one backend. Key credentials by account so they don't overwrite each other, and never log them.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developer.vario-software.de/documentation/apps/app-backends/banking.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
