.spec/` namespace. The
exact key varies by connector — Routable uses
`com.routable.spec/payment_initiation_reference`, Stripe uses
`com.stripe.spec/transfer_initiation_ref`, etc. See the per-connector
reference for the canonical key.
```bash
curl -s "$STACK/api/payments/v3/payments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"$match": {"connectorID": "'$CONNECTOR_ID'"}}' \
| jq '.cursor.data[] | { reference, pi_ref: .metadata."com.routable.spec/payment_initiation_reference" }'
```
## Deleting
" }} />
Only valid in `WAITING_FOR_VALIDATION`. Once an initiation has been
dispatched to the connector, it stays in the platform indefinitely so
the linked payment(s) and adjustment log remain queryable.
## Connector support matrix
Initiation requires `CAPABILITY_CREATE_TRANSFER` or
`CAPABILITY_CREATE_PAYOUT` on the target connector. See
[Capabilities](/modules/payments/capabilities) for the live matrix
— the columns light up per connector and per Payments version. Reversal
is available on connectors that additionally implement `ReverseTransfer`
or `ReversePayout`; that subset is smaller.
---
## Sources
Source: https://docs.formance.com/modules/numscript/reference/sources
There are several options when it comes to deciding _where_ the money should come from. The `send` statement gives you the following possibilities:
## Single source
The simplest way of sending a monetary value is from a single source. Here, we draw `COIN 100` from the `world` account:
```numscript
send [COIN 100] (
source = @world
destination = @users:001
)
```
## Ordered sources
Using an ordered source block, you can define several accounts to draw from sequentially until the desired monetary value is reached.
```numscript
send [COIN 100] (
source = {
@users:001:wallet
@payments:001
}
destination = @orders:001
)
```
In the example above, if the balance of `COIN` on the account `users:001:wallet` is 30, another 70 will be drawn from the `payments:001` account.
Ordered sources can also be capped with a monetary value, so that no more than the specified amount is drawn from them:
```numscript
send [COIN 100] (
source = {
max [COIN 10] from @users:001:wallet
@payments:001
}
destination = @orders:001
)
```
## Portioned sources
In addition to sequential accounts, source blocks can also use fractions to split the expense across multiple accounts.
In any case, the summed total of fractions in a block needs to be equal to 1 and the `remaining` keyword can be used to reach that total.
```numscript
send [COIN 100] (
source = {
10/100 from @platform:marketing
remaining from @users:001:wallet
}
destination = @orders:001
)
```
Out of convenience, percentage notation is also available:
```numscript
send [COIN 100] (
source = {
10% from @platform:marketing
remaining from @users:001:wallet
}
destination = @orders:001
)
```
## Nested sources
Source blocks can be nested with a combination of recursive ordered / portioned specifications:
```numscript
send [COIN 100] (
source = {
50% from {
max [COIN 10] from @users:001:wallet
@users:001:chest
}
remaining from @payments:001
}
destination = @orders:001
)
```
---
## Destinations
Source: https://docs.formance.com/modules/numscript/reference/destinations
As with sources, there are several options when it comes to deciding where the funds in a financial transaction should go. The `send` statement provides the following ways of defining destinations:
## Single destination
```numscript
send [COIN 100] (
source = @world
destination = @users:001
)
```
## Allocation destinations
Similar to portioned sources, destinations can be defined as a sequence of fractions that splits the monetary value across multiple accounts.
In any case, the summed total of fractions in a block needs to be equal to 1 and the `remaining` keyword can be used to reach that total:
```numscript
send [COIN 100] (
source = @world
destination = {
90/100 to @users:001
remaining to @fees
}
)
```
Out of convenience, percentage notation is also available:
```numscript
send [COIN 100] (
source = @world
destination = {
90% to @users:001
remaining to @fees
}
)
```
### Kept destinations
Instead of transferring all funds to new accounts, you can keep part of the amount in the source account with the `kept` keyword. It stands in for a destination account inside a block, and pairs naturally with `remaining` to keep whatever is left after the explicit allocations:
```numscript
send [COIN 100] (
source = @world
destination = {
50% to @users:001
remaining kept
}
)
```
Here 50% of the amount goes to `users:001` and the remaining 50% stays in `world`. This is useful when you only want to transfer a portion of the funds.
### Ordered destinations with maximum caps
Ordered destinations route funds to multiple accounts in sequence, sending up to a maximum cap to each one before moving on. Any leftover goes to the final `remaining` destination:
```numscript
send [COIN 100] (
source = @world
destination = {
max [COIN 20] to @users:001
max [COIN 50] to @users:002
remaining to @users:003
}
)
```
This sends COIN 20 to `users:001`, COIN 50 to `users:002`, and the remaining COIN 30 to `users:003`.
## Nested destinations
Finally, as with sources, destination blocks can be nested:
```numscript
send [COIN 100] (
source = @world
destination = {
80% to @users:001
20% to {
70% to @platform
15% to @taxes
remaining to @charity
}
}
)
```
---
## Rounding
Source: https://docs.formance.com/modules/numscript/reference/rounding
There is no support for floating point or decimal numbers in Numscript, which will always make sure non integer values resulting from monetary computations are balanced.
Practically, this means appropriately distributing the non integer allocation remainder to accounts. Numscript works by flooring any computed amount and subsequently spreading the remaining amount as fairly as possible starting from top to bottom.
In the example below:
```numscript
send [COIN 99] (
source = @world
destination = {
50% to @rider
50% to @taxes
}
)
```
The `@rider` account will receive `COIN 50` and the `@taxes` account `COIN 49`. The opposite can be achieved by reversing the order of destinations:
```numscript
send [COIN 99] (
source = @world
destination = {
50% to @taxes
50% to @rider
}
)
```
In a more complex example below, we are splitting 99 into 5 which would result in 19.8 allocated to each account. Numscript will first allocate 19 to every account, then attempt to distribute the remaining 4 evenly starting from `@a`:
```numscript
send [COIN 99] (
source = @world
destination = {
1/5 to @a
1/5 to @b
1/5 to @c
1/5 to @d
1/5 to @e
}
)
```
Which will resolve into the following postings:
```json
[
{
"source": "world",
"destination": "a",
"amount": 20,
"asset": "COIN"
},
{
"source": "world",
"destination": "b",
"amount": 20,
"asset": "COIN"
},
{
"source": "world",
"destination": "c",
"amount": 20,
"asset": "COIN"
},
{
"source": "world",
"destination": "d",
"amount": 20,
"asset": "COIN"
},
{
"source": "world",
"destination": "e",
"amount": 19,
"asset": "COIN"
}
]
```
## Fixed fees and allocation order
When combining fixed amounts with percentage-based allocations, the order of destinations matters due to the multi-pass resolution mechanism.
### The problem
Consider a transaction splitting funds between a payment provider (fixed fee + percentage), a franchise fee (percentage), and a store (remaining):
```numscript
send [AUD/2 1999] (
source = @world
destination = {
7/1999 to @payment_provider
0.6% to @payment_provider
0.5% to @franchise_fee
remaining to @store
}
)
```
You might expect `@payment_provider` to receive exactly 7 cents as a fixed fee, but it actually receives **8 cents**. This happens because of the two-pass allocation mechanism.
### How multi-pass allocation works
**First pass** - Numscript allocates whole amounts:
- `7/1999 * 1999 = 7` → `@payment_provider`
- `0.6% * 1999 = 11.994` → floors to `11` for `@payment_provider` (keeps 0.994 aside)
- `0.5% * 1999 = 9.995` → floors to `9` for `@franchise_fee` (keeps 0.995 aside)
- `remaining = 1999 - 7 - 11 - 9 - ceil(0.994 + 0.995) = 1970` → `@store`
**Second pass** - Numscript distributes the remaining fragments:
- Total distributed: `1999 - 2 = 1997`
- Remaining to distribute: `2 cents`
- Distribution is **top to bottom**: first position (`@payment_provider`) gets +1, second position (`@payment_provider` again) gets +1
Result: `@payment_provider` receives `7 + 1 = 8` cents instead of the expected 7.
### The solution
Move the fixed fee **after** the percentage allocations. Since percentage computations generate at most 1 cent fragment each, placing them first ensures they absorb the remainder:
```numscript
send [AUD/2 1999] (
source = @world
destination = {
0.6% to @payment_provider
0.5% to @franchise_fee
7/1999 to @payment_provider
remaining to @store
}
)
```
Now the percentages receive any extra cents, and the fixed fee remains exactly 7 cents.
When mixing fixed amounts and percentages, place percentage-based allocations **before** fixed amounts to ensure fixed fees remain exact.
---
## Save
Source: https://docs.formance.com/modules/numscript/reference/save
It is sometimes helpful to prevent an account from going below a certain threshold balance. The `save` directive allows you to specify a minimum balance for an account, which is deducted from the account's available balance for the transaction.
```numscript
// Keep the closing balance of @merchants:1234 at or above [USD/2 100]
save [USD/2 100] from @merchants:1234
send [USD/2 500] (
source = @merchants:1234
destination = @payouts:T1891G
)
```
In this transaction example, even if the account `@merchants:1234` has an initial balance of `[USD/2 500]`, the transaction will fail as the account post-transaction balance would otherwise be less than `[USD/2 100]`.
If an additional source of funds is provided, the account behaves as if its balance is `[USD/2 100]` less than it actually is:
```numscript
// Keep the closing balance of @merchants:1234 at or above [USD/2 100]
save [USD/2 100] from @merchants:1234
send [USD/2 500] (
source = {
@merchants:1234
@world
}
destination = @payouts:T1891G
)
```
```json
[
{
"source": "merchants:1234",
"destination": "payouts:T1891G",
"amount": 400,
"asset": "USD/2"
},
{
"source": "world",
"destination": "payouts:T1891G",
"amount": 100,
"asset": "USD/2"
}
]
```
## Insufficient funds error
When the requested amount exceeds the available balance (after applying `save`), the transaction fails with an `INSUFFICIENT_FUND` error.
**Example:** Account has `[GBP/2 120]`, save `[GBP/2 100]`, available = `[GBP/2 20]`
```numscript
save [GBP/2 100] from @my_account
send [GBP/2 30] (
source = @my_account
destination = @world
)
```
This fails because you're trying to send 30 but only 20 is available:
```json
{
"errorCode": "INSUFFICIENT_FUND",
"errorMessage": "running numscript: script execution failed: account(s) @my_account had/have insufficient funds"
}
```
## Using save with send [ASSET *]
When using `send [ASSET *]` (send entire balance) with `save`, the behavior depends on the account balance:
### Balance greater than save amount
If the balance exceeds the saved amount, the transaction sends `balance - saved_amount`:
```numscript
// Account balance: [GBP/2 120]
save [GBP/2 100] from @my_account
send [GBP/2 *] (
source = @my_account
destination = @world
)
```
Result: A transaction of `[GBP/2 20]` is created (120 - 100 = 20).
### Balance less than or equal to save amount
If the balance is less than or equal to the saved amount, a transaction with **amount 0** is created:
```numscript
// Account balance: [GBP/2 80]
save [GBP/2 100] from @my_account
send [GBP/2 *] (
source = @my_account
destination = @world
)
```
Result:
```json
{
"postings": [
{
"amount": 0,
"asset": "GBP/2",
"destination": "world",
"source": "my_account"
}
]
}
```
A transaction with 0 amount is still created and recorded in the ledger. This can be useful for audit purposes but may need to be filtered out in reporting.
## Multiple source accounts with save
When using multiple sources with `save`, funds are taken from the saved account up to its available limit, then completed from other sources:
```numscript
// @account_a balance: [USD/2 150]
save [USD/2 100] from @account_a
send [USD/2 80] (
source = {
@account_a
@account_b
}
destination = @destination
)
```
Result:
- `@account_a` contributes `[USD/2 50]` (its available balance: 150 - 100)
- `@account_b` contributes `[USD/2 30]` (remainder needed)
---
## Overdraft
Source: https://docs.formance.com/modules/numscript/reference/overdraft
The `overdraft` directive lets you instruct the Numscript interpreter that an account's post-transaction balance is allowed to be less than zero.
## Unbounded
The overdraft directive allows the account to go below zero without any limit, by using the `unbounded` keyword:
```numscript
send [USD/2 100] (
source = @foo allowing unbounded overdraft
destination = @bar
)
```
## Bounded
The exact amount below zero to which the account is allowed to go can be specified by using the `up to` keyword with a monetary value:
```numscript
send [USD/2 100] (
source = @foo allowing overdraft up to [USD/2 50]
destination = @bar
)
```
## The overdraft() function
The `overdraft()` function is an experimental feature. While it is expected to remain available either in its current form or through an equivalent mechanism in future versions, you need to enable experimental features to use it.
When working with asset accounts that have negative balances, you can use the `overdraft()` function to safely check balances and perform transactions atomically.
### Overview
The `overdraft()` function returns the positive amount of overdraft when an account's balance is negative, or `0` if the balance is positive. This is particularly useful when working with asset accounts, which by design have negative balances in Formance.
### Usage
Here's how to use the overdraft function:
```numscript
#![feature("experimental-overdraft-function", "experimental-mid-script-function-call")]
vars {
monetary $acc_overdraft = overdraft(@account, USD/2)
}
// Use $acc_overdraft in your transaction logic
send $acc_overdraft (
source = @world
destination = @account
)
```
In this example:
- If `@account` has a balance of `[USD/2 -50]`, `$acc_overdraft` will be `[USD/2 50]`
- If `@account` has a balance of `[USD/2 100]`, `$acc_overdraft` will be `[USD/2 0]`
### Enabling the experimental feature
To use `overdraft()`, you need to enable two experimental features:
1. The experimental rewrite feature
2. The `experimental-overdraft-function` flag
See the [Numscript embedding documentation](/modules/numscript) for instructions on enabling these features.
### Alternative: Using balance()
If you cannot use the experimental features, note that the standard `balance()` function only works with non-negative balances and will fail if the account balance is negative.
```numscript
#![feature("experimental-mid-script-function-call")]
vars {
// This will fail if @account has a negative balance
monetary $bal = balance(@account, USD/2)
}
```
For accounts that may have negative balances (like asset accounts), use the `overdraft()` function instead.
## Setting Overdrafts via API
In addition to specifying overdrafts in Numscript, you can also configure overdraft limits directly via the API when creating transactions.
### Using the transaction endpoint
When sending a transaction via the API, you can specify overdraft allowances in the script's `vars` section:
```json
{
"script": {
"vars": {
"amount": "USD/2 1000"
},
"plain": "send $amount ( source = @customers:123 allowing unbounded overdraft destination = @merchants:456 )"
}
}
```
### Bounded overdrafts via API
For bounded overdrafts, include the limit in the Numscript:
```json
{
"script": {
"vars": {},
"plain": "send [USD/2 1000] ( source = @customers:123 allowing overdraft up to [USD/2 500] destination = @merchants:456 )"
}
}
```
When using the API, the overdraft behavior is the same as when using Numscript directly. The API is simply a transport mechanism for the Numscript execution.
## Overdraft Validation with Backdated Transactions
When inserting backdated transactions with overdrafts, the ledger validates the final state rather than intermediate states. See [bi-temporality](/modules/ledger/working-with/bi-temporality#backdated-transaction-validation) for details on how backdated transactions interact with overdraft limits.
---
## Account Pools
Source: https://docs.formance.com/modules/payments/cash-pools
A **cash pool** is a collection of payment accounts from one or more payment service providers that you want to manage as a single unit. Cash pools aggregate balances across multiple accounts, providing a unified view of your funds for financial reporting, treasury management, and reconciliation.
Cash pools can be created in two ways:
- **Static pools**: Explicitly specify which accounts to include. Membership is fixed until you edit the pool.
- **Dynamic pools**: Define a query that matches accounts at evaluation time. New accounts that match the query — including accounts discovered on a fresh connector cycle after the pool was created — are automatically included in the pool's next balance query. Accounts removed from the connector (uninstalled, or now non-matching) drop out the same way.
Dynamic pools resolve membership at every balance read, so a treasury dashboard pointed at a dynamic pool reflects the current set of matching accounts in real time without manual pool maintenance.
## Why cash pools?
When managing funds across multiple financial institutions, you often need to view and analyze balances from different accounts together. For example, you might have:
- A Stripe account for card payments
- A PayPal account for alternative payments
- A bank account for direct transfers
Rather than querying each account individually, cash pools let you:
- **View aggregated balances** across all accounts in real-time
- **Track historical balances** at specific points in time
- **Simplify financial reporting** with consolidated balance views
- **Enable reconciliation** against your internal ledger
- **Monitor liquidity** across multiple financial institutions
- **Automatically include new accounts** that match your criteria (dynamic pools)
## Pool structure
A cash pool contains:
- **ID**: Unique UUID identifier
- **Name**: Human-readable name (must be unique)
- **Created at**: Timestamp when the pool was created
- **Pool accounts**: Array of account IDs included in the pool (for static pools)
- **Query**: JSON query defining account matching criteria (for dynamic pools)
## Creating cash pools
Cash pools are created via the Payments API using one of two approaches:
### Static pools
Specify exact accounts to include in the pool:
### Dynamic pools
Define criteria that automatically match accounts:
Static pools require explicit account IDs, while dynamic pools use JSON query syntax. The two approaches are mutually exclusive - you cannot specify both `accountIDs` and `query` in the same request.
## Query syntax
Dynamic pools use the same query syntax as the [List Accounts API](/stack-api-reference/paymentsv3/v3-list-accounts). Supported query fields include:
- **connector_id**: Match accounts from specific connectors
- **default_asset**: Filter by currency/asset
- **type**: Account type (e.g., `INTERNAL`, `EXTERNAL`)
- **name**: Account name matching
- **psu_id**: Payment service user ID
- **metadata.\{key\}**: Custom metadata fields
### Query examples
**All EUR accounts across connectors:**
```json
{
"name": "EUR Accounts",
"query": "{\"$match\": {\"default_asset\": \"EUR\"}}"
}
```
**Stripe accounts only:**
```json
{
"name": "Stripe Pool",
"query": "{\"$match\": {\"connector_id\": \"stripe:connector-123\"}}"
}
```
**Internal accounts with specific metadata:**
```json
{
"name": "Business Accounts",
"query": "{\"$match\": {\"type\": \"INTERNAL\", \"metadata.category\": \"business\"}}"
}
```
## Static vs. Dynamic pools
| Feature | Static Pools | Dynamic Pools |
|---------|-------------|-------------------|
| **Account membership** | Static - explicitly defined | Dynamic - automatically updated |
| **New accounts** | Must be manually added | Automatically included if they match criteria |
| **Use case** | Known, fixed set of accounts | Accounts that change frequently or match patterns |
| **Management** | Requires manual updates | Self-maintaining |
| **Account operations** | Can add/remove individual accounts | Cannot modify - accounts determined by query |
### When to use static pools
- **Fixed account sets**: When you have a specific, unchanging group of accounts
- **Mixed criteria**: When accounts don't follow a consistent pattern
- **Fine-grained control**: When you need to include/exclude specific accounts manually
### When to use dynamic pools
- **Dynamic environments**: When new accounts are frequently created
- **Connector-based grouping**: All accounts from a specific payment provider
- **Currency segregation**: Separate pools for different currencies
- **Automated workflows**: When pools need to be configured declaratively via API
Dynamic pools do not support manual account addition or removal. To modify membership, update the pool's query criteria.
## Using cash pools
### Balance queries
Pool balance endpoints aggregate balances from all accounts in the pool by asset:
**Latest aggregated balances:**
" }} noFctl />
**Historical balances at a specific timestamp:**
" }} query={{ at: "2024-01-15T23:59:59Z" }} noFctl />
Balance aggregation logic:
1. **Static pools**: Fetches balances from the specified account IDs
2. **Dynamic pools**: Runs the query to find matching accounts, then fetches their balances
3. Groups by asset/currency
4. Sums amounts for each asset
5. Returns array of aggregated balances
Dynamic pools resolve their account membership dynamically at query time, ensuring balances always reflect the current set of accounts matching your criteria.
Using pools with Reconciliation
Cash pools can be used as balance sources in `ledger_vs_pool_drift` and `source_parity` [control templates](/modules/reconciliation/controls). Rules can compare a pool with Ledger or with another pool, use a per-asset tolerance, and read each source at an independent point in time.
```json
{
"templateKind": "source_parity",
"templateSpec": {
"left": {
"kind": "ledger",
"ledger": "main",
"query": { "$match": { "address": "control:provider" } }
},
"right": {
"kind": "payments_pool",
"poolID": ""
},
"tolerance": { "USD/2": 0 }
}
}
```
See [Getting Started with Reconciliation](/modules/reconciliation/getting-started) for the complete rule and evaluation workflow.
Cash pool IDs are used in the `paymentsPoolID` field of [reconciliation policies](/modules/reconciliation/concepts#policies):
```json
{
"name": "string",
"ledgerName": "string",
"ledgerQuery": "object",
"paymentsPoolID": "uuid-string"
}
```
The reconciliation service compares ledger account balances against the aggregated cash pool balances. See [Getting Started with Reconciliation](/modules/reconciliation/getting-started) for a complete workflow.
---
## Variables
Source: https://docs.formance.com/modules/numscript/reference/variables
Hardcoded values are great for quick prototyping and iteration, but chances are that you will at some point need to inject some variables into your Numscript files.
Here is an example using definitions of all the supported variable types:
```numscript
vars {
monetary $price
account $trade
portion $commission
asset $pair
number $id
string $reference
}
send $price (
source = @world
destination = {
$commission to @platform
remaining to $trade
}
)
set_tx_meta("asset", $asset)
set_tx_meta("id", $id)
set_tx_meta("reference", $reference)
```
Injections of variables can be done at execution, by using the `POST /{ledger}/transactions` endpoint.
The `script.vars` field in the request body is used to inject variable values:
```json
{
"script": {
"vars": {
"price": "USD/2 100",
"trade": "trades:108391999",
"commission": "15%",
"pair": "EUR/2",
"id": "108391999",
"reference": "USD/EUR:108391999"
}
}
}
```
Variable names must be lowercase and start with at least one letter or `_`.
They can contain letters, digits, and `_`.
## Account type
This type represents account names. They must start with at least one letter or `_`.
They can contain letters, digits, `_`, and `:`.
## Asset type
This type represents asset names. They can contain uppercase letters, digits and `/`.
## Monetary type
This type represents a positive integer amount associated with an asset.
```json
"USD/2 100"
```
It is possible to pull the balance of an account and inject it in a monetary variable by using the `balance(_account_, _asset_)` statement.
The balance pulled needs to be non-negative, or the script will fail to execute. Here is an example:
```numscript
#![feature("experimental-mid-script-function-call")]
vars {
monetary $initial = balance(@A, USD/2)
}
send [USD/2 100] (
source = {
@A
@C
}
destination = {
max $initial to @B
remaining to @D
}
)
```
## Portion type
This type represents portions of monetary values.
They can be expressed in 2 different ways:
* As a percentage: `15%`
* As a fraction: `15/100`
Their computed values must be between 0 and 1 (inclusive).
Variables can also be pulled from account metadata, as described in the [metadata](/modules/numscript/reference/metadata) section.
---
## Metadata
Source: https://docs.formance.com/modules/numscript/reference/metadata
Numscript transactions can interact with metadata, both on transactions and accounts.
## Account metadata
### Reading metadata to initialize a variable
Structured account metadata can be injected in Numscript variables during initialization.
In the example below, we inject the monetary value stored under the metadata key `"coupon_value"` from the `coupon` account:
```numscript
#![feature("experimental-mid-script-function-call")]
vars {
account $coupon
account $wallet
monetary $value = meta($coupon, "coupon_value")
}
send $value (
source = $coupon
destination = $wallet
)
```
Metadata injected into variables needs to be typed, and its type is read from the value of the key `type` of the object stored under the metadata key.
Its value is read from the value of the key `value`. Here are all the available types:
```json
{
"amount": {
"type": "number",
"value": 1000
}
}
```
```json
{
"reference": {
"type": "string",
"value": "82HHON80ILP"
}
}
```
```json
{
"currency": {
"type": "asset",
"value": "USD/2"
}
}
```
```json
{
"coupon_value": {
"type": "monetary",
"value": {
"amount": 1000,
"asset": "USD/2"
}
}
}
```
```json
{
"merchant": {
"type": "account",
"value": "platform:merchant"
}
}
```
```json
{
"commission": {
"type": "portion",
"value": "15.5%"
}
}
```
### Writing account metadata during a transaction
Metadata can be written to an account using the `set_account_meta(_account_, "key", _value_)` statement.
The statement takes a string-type key and a value which can be of any type, either as a variable or a literal.
## Writing metadata to a transaction
Metadata can be written to a transaction using the `set_tx_meta("key", _value_)` statement.
The statement takes a string-type key and a value which can be of any type, either as a variable or a literal.
```numscript
set_tx_meta("order_fee", [USD/2 100])
set_tx_meta("tax", 20/100)
set_tx_meta("collection_account", @platform:commission)
set_tx_meta("commission", $commission)
```
## Wrap-up example
```numscript
#![feature("experimental-mid-script-function-call")]
vars {
account $order
account $merchant = meta($order, "merchant")
monetary $fee
portion $commission
string $ref
}
send $fee (
source = @orders:1234
destination = @platform:fees
)
send [USD/2 *] (
source = @orders:1234
destination = {
$commission to @platform:fees
remaining to $merchant
}
)
set_account_meta($order, "reference", $ref)
set_tx_meta("order_fee", $fee)
set_tx_meta("tax", 20/100)
set_tx_meta("commission", $commission)
```
```json
{
"script": {
"vars": {
"order": "orders:186HH78UH",
"fee": {
"amount": 1000,
"asset": "USD/2"
},
"commission": "15.5%",
"reference": "108IUYGI"
}
}
}
```
---
## Unambiguous Monetary Notation
Source: https://docs.formance.com/modules/numscript/monetary-notation
The Formance Platform uses a unified, safe-by-design way of representing monetary values across all its services and components. We call this representation the _Unambiguous Monetary Notation_, or UMN for short.
While you can use any `[A-Z]{1,16}(\/\d{1,6})` asset in your ledger transactions, it is encouraged to always use UMN, especially if you're dealing with any of the standardized [ISO-4217](https://en.wikipedia.org/wiki/ISO_4217) currencies.
## Specification
A UMN value is represented as:
```text
[ASSET/SCALE AMOUNT]
```
Where:
* `ASSET` is a string of 1 to 16 uppercase letters, representing the currency code of the asset, either standardized or fictional.
* `SCALE` represents the negative power of ten by which the amount is multiplied to obtain the decimal value in the given asset
* `AMOUNT` is an unsigned integer.
As an example `[USD/2 30]` is equivalent to `USD 30*1E-2`, i.e `USD 0.30`, i.e 30 USD cents.
For values where the amount already represents the amount of said asset, a scale of zero should not be represented, e.g. `[JPY 100]`.
## Precision
The UMN specification does not enforce a specific precision of the amount, beyond the fact that it must be represented as an unsigned integer. Decisions on the precision of the amount are left to the implementation when implemented by a third party. Internally, Formance Stack components all use arbitrary precision unsigned integers to represent amounts.
## Examples
| UMN | Human Readable | ISO-4217 code |
| --- | --- | --- |
| `[USD/2 30]` | `$0.30` | `USD` |
| `[JPY 100]` | `¥100` | `JPY` |
| `[BTC/8 100000000]` | `1 BTC` | `BTC` |
| `[GBP/2 100]` | `£1.00` | `GBP` |
| `[EUR/2 100]` | `€1.00` | `EUR` |
| `[INR/2 100]` | `₹1.00` | `INR` |
| `[CNY/2 100]` | `¥1.00` | `CNY` |
| `[CAD/2 100]` | `CA$1.00` | `CAD` |
While `USD/2` is a reasonable notation for most USD-handling use-cases, nothing prevents you from using `USD/4` or `USD/6` if you need to represent smaller amounts and subdivisions of USD in your system. The same applies to other currencies, e.g. `JPY/2` or `JPY/4` for Japanese Yen and while such a coin is not in circulation, it is still a valid notation when these amounts are used in a context where they will end up being floored or ceiled to the nearest whole unit later down the line.
## Rationale
The reason behind this recommendation is that using non explicitly scaled currencies like `USD` is inherently ambiguous, with interpretation of the scale left as an exercise to the reader.
If you receive from a payment processor an API response as follows:
```json
{
"amount": 100,
"currency": "USD"
}
```
Without more context, it is unfortunately impossible to tell whether the amount is in cents, or in dollars. While best practices dictate that the amount should be denominated in the smallest unit of the currency, this is not always the case as this interpretation is not standardized across payments services providers.
Some services will inevitably use different formats and encoding rules, resulting in situations where both `100`, and `100.30` are happily parsed, leaving the door open to catastrophic consequences.
As you start to scale your business and deal with multiple and specialized payment services providers, the risk of different formats making their way to your internal representation increases along with the risk of misinterpreting the amount.
As Formance components are designed to be used in a variety of contexts and find themselves dealing with a variety of formats from different providers, we decided to explicitly specify the scale of the amount in the notation, making UMN really hard to misinterpret by design.
---
## CLI
Source: https://docs.formance.com/modules/numscript/cli
## Install
You can install the `numscript` CLI in one of the following ways:
#### Using curl
For Mac and Unix:
```sh
curl -sSf https://raw.githubusercontent.com/formancehq/numscript/main/install.sh | bash
```
#### Using golang toolchain
```sh
go install github.com/formancehq/numscript/cmd/numscript@latest
```
## Check
You can use the `numscript check` command to run static analysis on a numscript program. The static analysis includes parsing errors, wrong variables or types usage, as well as more advanced checks on numscript constructs.
You can use it this way:
```shellscript
numscript check my-file.num
```
The command will exit with an error status code if there is at least one error or warning.
## Run
Available from numscript@0.0.19
You can use the CLI to run local scripts (mostly intended for local prototyping).
For example, given this script:
```numscript my-script.num
vars {
monetary $amt
}
send $amt (
source = @alice
destination = @world
)
```
And this inputs file (which has to have the same name as the numscript file, plus the `.inputs.json` suffix):
```json my-script.num.inputs.json
{
"$schema": "https://raw.githubusercontent.com/formancehq/numscript/refs/heads/main/v1.inputs.schema.json",
"variables": {
"amt": "USD/2 100"
},
"balances": [
{ "account": "alice", "asset": "USD/2", "amount": 9999 }
]
}
```
This format is available from numscript@0.0.25. If you're on an older version, use the [legacy inputs schema](https://raw.githubusercontent.com/formancehq/numscript/refs/heads/main/inputs.schema.json) instead — it uses a map instead of an array for `balances` (and `metadata`).
You can run the file using:
```shellscript
numscript run my-script.num
```
You'll see the postings:
```
Postings:
| Source | Destination | Asset | Amount |
| alice | world | USD/2 | 100 |
```
## Test
Available from numscript@0.0.19
You can use the `numscript test` command to check that the specs given in a [numscript specs format](/modules/numscript/specs) file are valid for a given numscript.
For example:
```shellscript
numscript test src/domain/numscript
```
This looks through the `src/domain/numscript` folder for every `.num` that has a matching `.num.specs.json` specs file.
---
## Functions
Source: https://docs.formance.com/modules/numscript/reference/functions
Built-in functions callable from Numscript. Which functions you can use depends on the interpreter version bundled in your Ledger release — see [Selecting an Interpreter](/modules/numscript/interpreter) for the full availability matrix.
The entries below reflect your current selection: each experimental function shows whether it is available for the interpreter version you have selected.
## Statements
These are available on both the original (`machine`) and experimental interpreters.
### `set_tx_meta("key", value)`
Writes metadata to the transaction. The key is a string; the value can be any type, as a literal or a variable. [Details](/modules/numscript/reference/metadata)
**Available from:** all versions (`machine` and `experimental-interpreter`).
### `set_account_meta(account, "key", value)`
Writes metadata to an account during the transaction. [Details](/modules/numscript/reference/metadata)
**Available from:** all versions (`machine` and `experimental-interpreter`).
## Functions in `vars`
These functions are called when initializing variables in the `vars` block. They require the experimental interpreter (`runtime: experimental-interpreter`) and the `experimental-mid-script-function-call` flag.
### `meta(account, "key")`
Reads structured account metadata into a typed variable. [Details](/modules/numscript/reference/metadata)
**Available from:** interpreter `0.0.15` · Ledger 2.3 · Stack v3.1 · flag `experimental-mid-script-function-call`
Not available in your selected version (requires interpreter 0.0.15+).}>
Available in your selected version.
### `balance(account, asset)`
Returns the balance of an account for an asset. Fails on negative balances — use `overdraft()` for accounts that may go negative. [Details](/modules/numscript/reference/mid-script-functions)
**Available from:** interpreter `0.0.15` · Ledger 2.3 · Stack v3.1 · flag `experimental-mid-script-function-call`
Not available in your selected version (requires interpreter 0.0.15+).}>
Available in your selected version.
### `overdraft(account, asset)`
Returns the positive overdraft amount of an account, or zero if the balance is non-negative. [Details](/modules/numscript/reference/overdraft)
**Available from:** interpreter `0.0.15` · Ledger 2.3 · Stack v3.1 · flags `experimental-overdraft-function`, `experimental-mid-script-function-call`
Not available in your selected version (requires interpreter 0.0.15+).}>
Available in your selected version.
### `get_asset(monetary)`
Returns the asset part of a monetary value. [Details](/modules/numscript/reference/get-asset)
**Available from:** interpreter `0.0.16` · Ledger 2.3 · Stack v3.1 · flag `experimental-get-asset-function`
Not available in your selected version (requires interpreter 0.0.16+).}>
Available in your selected version.
### `get_amount(monetary)`
Returns the numeric amount part of a monetary value. [Details](/modules/numscript/reference/get-amount)
**Available from:** interpreter `0.0.16` · Ledger 2.3 · Stack v3.1 · flag `experimental-get-amount-function`
Not available in your selected version (requires interpreter 0.0.16+).}>
Available in your selected version.
---
## Numscript specs format
Source: https://docs.formance.com/modules/numscript/specs
The Numscript specs format is a conventional way to express unit tests about Numscript, using JSON. It can be used to define assertions over the results of a numscript run, given certain inputs. You can execute the tests using the [`numscript test`](/modules/numscript/cli#test) command.
A JSON schema is available [online](https://raw.githubusercontent.com/formancehq/numscript/refs/heads/main/v1.specs.schema.json), so that you can have autocomplete and diagnostics in your editor. In many editors, such as VS Code, you can enable it by adding it to the JSON like this:
```json
{
"$schema": "https://raw.githubusercontent.com/formancehq/numscript/refs/heads/main/v1.specs.schema.json"
}
```
This format is available from numscript@0.0.25. If you're on an older version, use the [legacy specs schema](https://raw.githubusercontent.com/formancehq/numscript/refs/heads/main/specs.schema.json) instead — it uses maps instead of arrays for `balances` and `metadata`.
Here's the schema (using typescript notation):
```typescript
type Specs = {
balances?: Balances;
variables?: Vars;
metadata?: AccountsMetadata;
featureFlags?: Array;
testCases: Array
};
type TestCase = {
balances?: Balances;
variables?: Vars;
metadata?: AccountsMetadata;
it: string;
"expect.error.missingFunds"?: boolean;
"expect.error.negativeAmount"?: boolean;
"expect.postings"?: Array;
"expect.txMetadata"?: TxMetadata;
"expect.metadata"?: SetAccountsMetadata;
"expect.endBalances"?: Balances;
"expect.endBalances.include"?: Balances;
"expect.movements"?: Movements;
}
```
```typescript
type Balances = Array;
type BalanceRow = {
account: string;
asset: string;
amount: number;
// Optional, for setups using asset colors or scoped accounts
color?: string;
scope?: string;
};
type Vars = {
[name: string]: string
};
type AccountsMetadata = Array;
type AccountMetadataRow = {
account: string;
key: string;
value: string;
scope?: string;
};
// Used by `expect.metadata`. The value is the metadata's rendered form (see
// the note under `expect.txMetadata`), same shape as the `metadata` precondition.
type SetAccountsMetadata = Array;
type SetAccountMetadataRow = {
account: string;
key: string;
value: string;
scope?: string;
};
type TxMetadata = Array;
type TxMetadataRow = {
key: string;
value: string;
};
type Posting = {
source: string;
destination: string;
asset: string;
amount: number;
sourceScope?: string;
destinationScope?: string;
color?: string;
};
type Movements = Array;
type Movement = {
source: string;
destination: string;
asset: string;
amount: number;
sourceScope?: string;
destinationScope?: string;
color?: string;
};
```
### Example
Say we have the following numscript:
```numscript
vars {
monetary $cap
account $source
account $destination
}
send [EUR/2 *] (
source = max $cap from $source
destination = $destination
)
```
And we want to test that we never send more than `$cap`. We can express the relevant test cases in the following way:
```json
{
"$schema": "https://raw.githubusercontent.com/formancehq/numscript/refs/heads/main/v1.specs.schema.json",
"variables": {
"source": "alice",
"destination": "bob"
},
"balances": [
{ "account": "alice", "asset": "EUR/2", "amount": 500 }
],
"testCases": [
{
"it": "sends all the available balance when it doesn't exceed the cap and @alice has enough balance",
"variables": {
"cap": "EUR/2 9999"
},
"expect.postings": [
{
"source": "alice",
"destination": "bob",
"amount": 500,
"asset": "EUR/2"
}
]
},
{
"it": "caps the sent amt to $cap when lower than available balance",
"variables": {
"cap": "EUR/2 10"
},
"expect.postings": [
{
"source": "alice",
"destination": "bob",
"amount": 10,
"asset": "EUR/2"
}
]
}
]
}
```
## Preconditions
The inputs of each test cases. You can set the preconditions top-level (in the outer object), and/or in each `testCase` . The preconditions in a testCase will be merged to the top-level preconditions (with the precedence being given to the inner preconditions).
Each entry in `balances` and `metadata` is keyed by its `account` (plus `asset` for balances, `key` for metadata, and optionally `color` / `scope`) — merging replaces entries with a matching key and keeps the rest.
For example, in the following specs:
```json
{
"balances": [
{ "account": "alice", "asset": "EUR/2", "amount": 100 },
{ "account": "alice", "asset": "USD/2", "amount": 100 },
{ "account": "bob", "asset": "EUR/2", "amount": -2 }
],
"testCases": [
{
"it": "example specs",
"balances": [
{ "account": "alice", "asset": "EUR/2", "amount": 999 }
]
}
]
}
```
The inner preconditions will only override `@alice` 's `EUR/2` balance, resulting in:
```json
[
{ "account": "alice", "asset": "EUR/2", "amount": 999 },
{ "account": "alice", "asset": "USD/2", "amount": 100 },
{ "account": "bob", "asset": "EUR/2", "amount": -2 }
]
```
### `variables`
The (stringified) value of each variable
```json
{
"variables": {
"amount": "USD/2 100"
}
}
```
### `balances`
The initial accounts' balances.
```json
{
"balances": [
{ "account": "alice", "asset": "USD/2", "amount": 200 },
{ "account": "bob", "asset": "USD/2", "amount": -42 }
]
}
```
### `metadata`
The initial accounts' metadata.
```json
{
"metadata": [
{ "account": "alice", "key": "id", "value": "1234" }
]
}
```
## Assertions
Assertions are only run if explicitly defined.
The recommended assertion to use by default are `expect.postings` or `expect.error.missingFunds`, but there are also a few weaker assertion that might be useful when the exact postings are an implementation detail of your business logic.
### `expect.error.missingFunds`
Assert that the script failed because of missing funds. Even if this is set to true, the test will still fail if the script outputs a different error.
Defaults to `false`.
Note: this was called `expect.error` in earlier releases
### `expect.error.negativeAmount`
Assert that the script failed because of a send statement using a negative amount.
Defaults to `false`.
### `expect.postings`
Assert against the exact postings emitted by the script. To assert that there are no postings, you can use the empty array. To assert that no postings are produced because of a failure due to missing funds, you can use the `expect.error.missingFunds` assertion instead.
```json
{
"expect.postings": [
{ "source": "world", "destination": "user:001", "asset": "EUR/2", "amount": 100 }
]
}
```
### `expect.txMetadata`
Assert against the transaction meta emitted by the script (using `set_tx_meta`). It's an array of entries, one per metadata key. Each `value` is the metadata's rendered form — a monetary renders as `"USD/2 100"`, a portion as `"1/2"`, an account as its bare name, and so on — rather than a type-tagged object. Because the wire form is untyped, values of different types that render alike (a string `"42"` and the number `42`) are indistinguishable here.
```json
{
"expect.txMetadata": [
{ "key": "senderAccount", "value": "user:5829" }
]
}
```
An unreleased build briefly represented this value as a type-tagged object, e.g. `{"type": "monetary", "asset": "USD/2", "amount": "100"}` instead of `"USD/2 100"`. That representation never reached a stable release and has been reverted — if a specs file still uses it, rewrite each `value` to its rendered string form.
### `expect.metadata`
Assert against the accounts metadata at the end of script execution (using `set_account_meta`). It's an array of entries, each keyed by `account` and `key`, with the same rendered `value` shape as `expect.txMetadata` (including the same short-lived tagged-object format, now reverted).
```json
{
"expect.metadata": [
{ "account": "alice", "key": "id", "value": "1234" }
]
}
```
Note that it takes into account the values defined with the `metadata` precondition as well.
### `expect.endBalances`
Assert against the balances at the end of the script
For example:
```json
{
"expect.endBalances": [
{ "account": "alice", "asset": "EUR/2", "amount": 100 }
]
}
```
means that `@alice` has `[EUR/2 100]` balance after the script is applied.
You might consider using this assertion when you only care about the end balance of an account, for example if you need to bring an account to a certain value (not less, not more), so you don't care about how the postings are composed exactly.
Note: this was called `expect.volumes` in earlier releases
### `expect.endBalances.include`
A weaker version of `expect.endBalances` that allows defining a subset of the balances we assert against.
For example, the following:
```json
{
"expect.endBalances.include": [
{ "account": "alice", "asset": "EUR/2", "amount": 100 }
]
}
```
passes even if there are more accounts in the involved balances, and if `alice` emit postings involving other currencies.
### `expect.movements`
Assert against the resulting movements. A movement is an array entry from a source account, to a destination account, for a given asset and amount.
For example, this assertion:
```json
{
"expect.movements": [
{ "source": "alice", "destination": "bob", "asset": "EUR/2", "amount": 100 }
]
}
```
means that `@alice` sent `[EUR/2 100]` to `@bob`
You might consider using this assertion when you care about the movements graph from-to accounts, and you don't care about the order of the postings or the way they are split.
## Focus mode
You can select a subset of test to run by using the `focus` and `skip` modifiers on a test case definition. They are only meant to be used while developing, and will produce an error status code so that they aren't committed by mistake thus producing false positive tests.
### `focus`
If at least a test has a `focus` modifier, all the tests without the `focus` modifier will be skipped.
```json
{
"testCases": [
{
"it": "only run this test!",
"focus": true,
"expect.postings": // ..
},
{
"it": "this test is skipped",
"expect.postings": // ..
}
]
}
```
### `skip`
If a test is marked with the `skip` modifier, it will not be run.
```json
{
"testCases": [
{
"it": "skip this test",
"skip": true,
"expect.postings": // ..
}
]
}
```
---
## Payment Service Users
Source: https://docs.formance.com/modules/payments/payment-service-users
A Payment Service User (PSU) represents an end-user in the Formance Payments system. PSUs serve as a way to associate accounts, payments, and connections with specific individuals or entities across different connector types.
## PSU Properties
| Field | Description |
|-------|-------------|
| **ID** | Unique identifier for the PSU |
| **Name** | User's full name (encrypted at rest) |
| **Contact Details** | Email, phone number, locale (encrypted at rest) |
| **Address** | Street name, street number, city, region, postal code, country (encrypted at rest) |
| **Bank Account IDs** | Associated traditional bank accounts |
| **Metadata** | Additional custom key-value pairs |
| **Created At** | Timestamp of PSU creation |
All personally identifiable information (name, contact details, address) is encrypted at rest to ensure compliance with data protection regulations.
## Connections
A connection represents the link between a PSU and their bank account(s) through a provider. Key characteristics:
- Usually, a user has one connection per bank
- There can be multiple accounts in a single connection
- The same account could appear in different connections (e.g. joint accounts, guardianship)
## Working with PSUs
For a complete walkthrough of creating PSUs, establishing connections, and accessing account data, see the [Open Banking Getting Started guide](/modules/payments/connectors/open-banking/getting-started).
---
## oneof
Source: https://docs.formance.com/modules/numscript/reference/oneof
Requires flag: `experimental-oneof`
Select the first source or destination that can satisfy the transaction. Unlike ordered sources (which split funds across accounts), `oneof` picks exactly one branch.
## Source
Try each account in order. The first one with sufficient balance handles the entire amount:
```numscript
#![feature("experimental-oneof")]
send [USD/2 10000] (
source = oneof {
@users:1234:main
@users:1234:savings
@world
}
destination = @merchants:5678
)
```
If `@users:1234:main` has 10000 or more, it's used entirely. If not, `@users:1234:savings` is tried. Falls through to `@world` only if neither user account can cover the full amount.
This differs from ordered sources (`{ @a @b @c }`) which would drain `@a` first, then take the remainder from `@b`, then `@c`. With `oneof`, it's all-or-nothing per branch.
## Destination
```numscript
#![feature("experimental-oneof")]
send [USD/2 10000] (
source = @revenue
destination = oneof {
max [USD/2 5000] to @payouts:priority
remaining to @payouts:standard
}
)
```
The first matching constraint is used. `max` caps the amount for that branch; `remaining` catches everything else.
---
## Payments
Source: https://docs.formance.com/modules/payments
For the mental model of how Payments, Ledger, and Reconciliation work together, see [How the Modules Fit Together](/getting-started/modules-fit-together).
## Getting started
Learn the core concepts you need to know to use the Payments service.
Find which capabilities does each provider support.
Install the Formance connector for your payment service provider.
Connect to Open Banking providers like Plaid, Tink, and Powens for secure bank account access.
---
## Account Interpolation
Source: https://docs.formance.com/modules/numscript/reference/account-interpolation
Requires flag: `experimental-account-interpolation`
Build account addresses dynamically by inserting variables into the address:
```numscript
#![feature("experimental-account-interpolation")]
vars {
string $user_id
string $type
}
send [USD/2 1000] (
source = @world
destination = @users:$user_id:wallets:$type
)
```
With `$user_id = "42"` and `$type = "main"`, the destination resolves to `@users:42:wallets:main`.
## Supported variable types
Account, string, and number variables can be interpolated. Each is coerced to a string and inserted into the address.
```numscript
#![feature("experimental-account-interpolation")]
vars {
number $id
account $org
}
send [USD/2 500] (
source = @world
destination = @orgs:$org:members:$id:balance
)
```
## Use cases
- Route to user-specific accounts without hardcoding addresses
- Build hierarchical account structures from transaction parameters
- Create dynamic chart-of-accounts patterns like `@orders:$order_id:payments:$payment_id`
---
## get_asset
Source: https://docs.formance.com/modules/numscript/reference/get-asset
Requires flag: `experimental-get-asset-function`
Returns the asset part of a monetary value.
```numscript
#![feature("experimental-mid-script-function-call", "experimental-get-asset-function")]
vars {
monetary $payment
asset $asset = get_asset($payment)
}
send $payment (
source = @world
destination = @users:1234
)
set_tx_meta("currency", $asset)
```
With `$payment = [EUR/2 5000]`, `$asset` resolves to `EUR/2`.
## Use cases
- Store the asset of a dynamic monetary variable in transaction metadata
- Route transactions differently based on currency
- Validate asset type before processing
---
## get_amount
Source: https://docs.formance.com/modules/numscript/reference/get-amount
Requires flag: `experimental-get-amount-function`
Returns the numeric amount part of a monetary value.
```numscript
#![feature("experimental-mid-script-function-call", "experimental-get-amount-function")]
vars {
monetary $payment
number $amount = get_amount($payment)
}
send $payment (
source = @world
destination = @users:1234
)
set_tx_meta("amount", $amount)
```
With `$payment = [EUR/2 5000]`, `$amount` resolves to `5000`.
## Use cases
- Store the amount of a dynamic monetary variable in transaction metadata
- Compute fees or splits based on the amount
- Log transaction amounts independently of the asset
---
## Mid-script Function Calls
Source: https://docs.formance.com/modules/numscript/reference/mid-script-functions
Requires flag: `experimental-mid-script-function-call`
Call functions like `balance()` directly in variable declarations, and use arithmetic expressions in the `vars` block.
## Function calls in vars
```numscript
#![feature("experimental-mid-script-function-call")]
vars {
monetary $current = balance(@users:1234, USD/2)
}
send $current (
source = @users:1234
destination = @savings:1234
)
```
Without this feature, `balance()` can only be used inline. With it, you can capture the result in a variable and reference it multiple times.
## Arithmetic expressions
```numscript
#![feature("experimental-mid-script-function-call")]
vars {
portion $fee = 80%
monetary $total = balance(@users:1234, USD/2)
}
send $total (
source = @users:1234
destination = {
$fee to @platform:fees
remaining to @merchants:5678
}
)
set_tx_meta("fee", $fee)
set_tx_meta("total", $total)
```
---
## Asset Colors
Source: https://docs.formance.com/modules/numscript/reference/asset-colors
Requires flag: `experimental-asset-colors`
Restrict which funds can be sourced from an account based on a color tag. Colors track the origin or purpose of funds — useful for compliance, earmarking, or regulatory constraints.
## Syntax
Use `\` after a source account to restrict by color:
```numscript
#![feature("experimental-asset-colors")]
send [COIN 100] (
source = @treasury \ "GRANT"
destination = @programs:education
)
```
Only funds tagged with the `"GRANT"` color in `@treasury` are used. If the account has 200 COIN total but only 80 are tagged `"GRANT"`, this transaction fails with insufficient funds.
## Ordered color fallback
Combine with ordered sources to try different colors in priority:
```numscript
#![feature("experimental-asset-colors")]
send [COIN 100] (
source = {
@treasury \ "GRANT"
@treasury \ "DONATION"
@treasury
}
destination = @programs:education
)
```
Try grant-colored funds first, then donation-colored, then uncolored funds. The
final entry is not an "any color" fallback: a source with no color restriction reads
the uncolored bucket only, so `@treasury` there is the same source as
`@treasury \ ""`. No single source draws across every color — to reach more buckets,
list each one as its own ordered entry.
## With overdraft
Color restrictions can be combined with overdraft:
```numscript
#![feature("experimental-asset-colors")]
send [COIN 100] (
source = @treasury \ "GRANT" allowing unbounded overdraft
destination = @programs:education
)
```
## Use cases
- Earmark funds for specific purposes (grants, donations, operating)
- Regulatory compliance — track fund origins through the ledger
- Prevent mixing of restricted and unrestricted funds
---
## Getting Started with Open Banking
Source: https://docs.formance.com/modules/payments/connectors/open-banking/getting-started
This guide walks you through implementing Open Banking with Formance, from creating users to accessing bank account data.
- An Open Banking connector installed ([Plaid](/modules/payments/connectors/open-banking/plaid), [Tink](/modules/payments/connectors/open-banking/tink), or [Powens](/modules/payments/connectors/open-banking/powens))
## Overview
The Open Banking workflow consists of these key steps:
1. **Create a Payment Service User (PSU)** - Represents your end user
2. **Forward PSU to Connector** - Register the user with your Open Banking provider
3. **Create Authentication Link** - Generate a secure URL for bank connection
4. **User Authentication** - User connects their bank account via the provider's interface
5. **Access Account Data** - Retrieve accounts, balances, and transactions
## Implementation
### Step 1: Create a Payment Service User
Create a [Payment Service User (PSU)](/modules/payments/payment-service-users) to represent the end user who will connect their bank account.
The fields shown below are exhaustive. Some providers may require only a subset of these fields, while others may require all of them. Check the connector docs for your specific provider's requirements.
**Response:**
```json
{
"data": "5968b0e2-06da-4552-8ad0-c484706bd2d7"
}
```
Save the returned PSU ID for subsequent steps.
### Step 2: Forward PSU to Connector
Register the PSU with your Open Banking connector to prepare them for authentication.
", connectorID: "" }} noFctl />
**Parameters:**
- `psuID`: The PSU ID from Step 1
- `connectorID`: Your Open Banking connector ID (find this in your Formance Console under Connectors)
**Response:**
```
204 No Content
```
### Step 3: Create Authentication Link
Generate a secure authentication URL for the user to connect their bank account.
", connectorID: "" }} noFctl
body={{ applicationName: "Your App Name", clientRedirectURL: "https://yourapp.com/banking" }} />
Check the connector docs for your provider's redirect URL requirements and restrictions.
**Response:**
```json
{
"attemptID": "xyz789",
"link": "https://secure.plaid.com/hl/authentication-link"
}
```
### Step 4: User Authentication Flow
Direct the user to the authentication link to connect their bank account.
**Your Application:**
Redirect the user to the `link` from Step 3
See [Frontend Integration Guidelines](#frontend-integration-guidelines) for detailed implementation guidance across different platforms.
**User Experience (Provider Interface):**
The user will:
1. Select their bank from the provider's interface and be redirected to the bank's interface
2. Enter credentials or complete OAuth flow with their bank
3. Grant permissions for account access
4. See confirmation that the connection was successful
**Return to Your Application:**
- The provider automatically redirects the user back to your `clientRedirectURL`
- Check the authentication status by requesting the link attempt:
", connectorID: "", attemptID: "" }} noFctl />
**Response:**
```json
{
"data": {
"id": "adc80553-02df-4d42-ad88-44099af38580",
"psuID": "7ab143dd-e686-4fbe-a64d-11f67b40985d",
"connectorID": "eyJQcm92aWRlciI6InBvd2VucyIsIlJlZmVyZW5jZSI6ImMxMTMyYjg0LTdmYTEtNDRhZS1hZmRjLTBjMWZjMjIyYTIyYSJ9",
"createdAt": "2025-09-25T15:05:04.316284Z",
"status": "completed",
"clientRedirectURL": "https://console.v3.staging.formance.cloud/knonmzexcoal/vayn?region=staging.formance.cloud",
"error": null
}
}
```
- Use the `status` field to determine if the authentication was successful
### Step 5: Access Connected Accounts
Once the connection is established, you can access the user's account data.
**List accounts for a specific PSU:**
**List all accounts (no filter):**
**Get specific account:**
" }} noFctl />
**Get account balances:**
" }} noFctl />
## Connection Management
### List User Connections
View all connections for a specific user and connector:
", connectorID: "" }} noFctl />
**Response:**
```json
{
"cursor": {
"pageSize": 15,
"hasMore": false,
"data": [
{
"connectionID": "conn_456def789",
"connectorID": "plaid_prod_001",
"createdAt": "2024-01-15T10:30:00Z",
"dataUpdatedAt": "2024-01-15T10:35:00Z",
"status": "ACTIVE",
"error": null,
"metadata": {}
}
]
}
}
```
### Monitor Connection Status
Set up webhook handlers to receive notifications about connection status changes. You'll receive events when users complete authentication, when new data is synced, or when connections are lost.
### Refresh Stale Connections
If a connection becomes stale, generate a new authentication link:
", connectorID: "", connectionID: "" }} noFctl
body={{ applicationName: "Your App Name", clientRedirectURL: "https://yourapp.com/banking" }} />
### Delete Operations
**Delete a specific connection:**
", connectorID: "", connectionID: "" }} noFctl />
**Delete entire user:**
" }} noFctl />
Deletion operations are permanent and cannot be undone. All related data (accounts, transactions, connections) will be permanently removed.
## Frontend Integration Guidelines
### Browser Integration
For web applications:
- **Use full page redirects** - Do not use iframes as they're not compatible with all bank redirections
- Follow provider-specific browser integration guidelines
### Mobile Integration
For mobile applications:
- **Android**: Use Chrome Custom Tabs for the authentication flow
- **iOS**: Use SFSafariViewController for the authentication flow
- Avoid using in-app webviews as they may not support all authentication flows
### Provider-Specific Guidelines
Each Open Banking provider has specific integration requirements:
- **[Plaid](https://plaid.com/docs/)**: Standard OAuth implementation with redirect URL restrictions
- **[Powens](https://docs.powens.com/api-reference/overview/webview#browser-integration)**: Specific webview browser integration guidelines
- **[Tink](https://docs.tink.com/resources/transactions/optimize-your-transactions-integration)**: Platform-specific optimization guides for Android and iOS
---
## Plaid
Source: https://docs.formance.com/modules/payments/connectors/open-banking/plaid
The Plaid connector links Formance to Plaid's bank-aggregation platform. It drives the [Open Banking PSU flow](/modules/payments/connectors/open-banking/getting-started): create a Payment Service User, forward them to Plaid, hand them a Plaid Link session, and sync the resulting accounts, balances, and transactions back through Payments. Available from Payments 3.2.0.
The Plaid connector requires Payments **3.2.0 or higher**. Your stack pins an older version — upgrade to use it.
## Prerequisites
You need a Plaid account and a `clientID` + `clientSecret` pair. The key needs access to Auth, Transactions, and Identity at minimum (set products on the Plaid dashboard before issuing the key).
Plaid signs every webhook with a JWT verified against Plaid's public key — no shared secret to configure.
## Installation
{"fctl payments connectors install plaid config.json"}
### Configuration fields
`isSandbox: true` flips to Plaid's sandbox host (`https://sandbox.plaid.com`); production uses `https://production.plaid.com`. No separate "development" environment.
## Capabilities
- `FETCH_ACCOUNTS` — depository, credit, and loan accounts per PSU connection.
- `FETCH_BALANCES` — available + current balance via Plaid's `/accounts/balance/get`.
- `FETCH_EXTERNAL_ACCOUNTS` — counterparty accounts identified by Plaid's `Transfer` product when present.
- `FETCH_PAYMENTS` — transactions via Plaid's incremental `/transactions/sync` cursor.
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — webhooks per Item (Plaid's connection primitive), provisioned on link.
The connector doesn't initiate transfers or payouts — Plaid's `Transfer` product is read-only here. Use a PSP connector (Stripe, Increase) for outbound flows.
## Account model
Every Payments internal account is one Plaid `Account` on a linked Item (depository, credit, or loan). The `reference` is the Plaid `account_id`; `name` is the Plaid account name; `defaultAsset` is the account's ISO currency at standard precision. Each account is PSU-scoped — `psuID` carries the Payments PSU and `openBankingConnectionID` carries the Plaid Item. EXTERNAL accounts come from Plaid's `Transfer` product when present. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Linking a user
Plaid's auth ceremony runs through Plaid Link. The Payments surface:
1. **Create a PSU** — `v3CreatePaymentServiceUser` returns a PSU ID.
2. **Forward to Plaid** — `v3ForwardPaymentServiceUserToProvider` calls `/link/token/create` and stores the `link_token` as PSU metadata.
3. **Create a Link session** — `v3CreateLinkForPaymentServiceUser` returns a public Link URL + `attemptID`.
4. **Frontend redirect** — the user picks a bank, authenticates, and lands on your `clientRedirectURL`.
5. **Item-creation webhook** — the connector exchanges the `public_token` for an `access_token` and stores it on the connection.
Step-by-step walkthrough on the [Open Banking Getting Started guide](/modules/payments/connectors/open-banking/getting-started).
### Redirect URL requirements
- **HTTPS** in production.
- **Registered** in the Plaid dashboard under **Developers → API → Allowed redirect URIs** before issuing a Link token targeting it. The connector doesn't auto-register; mismatches surface as `INVALID_OAUTH_STATE_PARAMETER` at the Link step.
- Mobile apps must use **Plaid's universal-link** patterns ([iOS](https://plaid.com/docs/link/ios/), [Android](https://plaid.com/docs/link/android/)). The connector returns the URL unchanged — universal-link routing is a frontend concern.
## Asset model
Multi-currency, formatted to UMN at ISO 4217 precision (`USD/2`, `EUR/2`, `GBP/2`). Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
Plaid transactions are non-stateful — Plaid surfaces them only after they post, so the connector emits each as `SUCCEEDED`. The `pending` flag maps to `PENDING` until Plaid clears it and the row's `pending_transaction_id` is replaced by a permanent `transaction_id`; at that point the connector swaps the reference and moves the row to `SUCCEEDED`.
## Metadata keys
Under `com.plaid.spec/`:
- **Account**: `account_id`, `mask` (account-number last4), `name`, `official_name`, `subtype` (`checking` / `savings` / `credit card` / …), `verification_status`.
- **External account**: `account_number_last4`, `routing_number`, `wire_routing_number` (where available).
- **Payment**: `transaction_id`, `pending_transaction_id` (when `pending=true`), `category`, `category_id`, `merchant_name`, `personal_finance_category.primary`, `payment_channel`, `iso_currency_code`.
PSU-level metadata also carries `user_token` and `link_token` — connector internals, not editable.
## Workflow tree
```text
FetchAccounts (periodic) — per PSU/Item
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per PSU/Item, /transactions/sync cursor
FetchExternalAccounts (periodic)
CreateWebhooks — one hook per Item, provisioned automatically on link
```
## Pagination and recovery
`/transactions/sync` is cursor-based and idempotent. The connector persists the latest cursor per Item in platform-managed `State`; restarts resume from the last committed cursor (Plaid's at-least-once semantics aside).
## Known gaps
- **Identity** and **Income** products aren't surfaced — only Auth, Transactions, and (when present) Transfer.
- **Investment accounts** are surfaced as accounts but their holdings are not — only depository balances and transactions land.
- **Webhooks** failing signature verification are dropped silently with a single-line log. Look for `plaid: webhook signature failed verification`.
---
## Tink
Source: https://docs.formance.com/modules/payments/connectors/open-banking/tink
The Tink connector links Formance to Tink's European bank-aggregation platform. It drives the [Open Banking PSU flow](/modules/payments/connectors/open-banking/getting-started) across Tink's markets: create a Payment Service User, forward them to Tink, hand them a Tink Link session, and sync the resulting accounts, balances, and transactions back through Payments. Available from Payments 3.2.0.
The Tink connector requires Payments **3.2.0 or higher**. Your stack pins an older version — upgrade to use it.
## Prerequisites
You need a Tink account and a `clientID` + `clientSecret` pair. Tink uses OAuth2 client-credentials; the connector exchanges the credentials for a short-lived bearer token and refreshes automatically.
Covered EU markets: AT, BE, DE, DK, EE, ES, FI, FR, GB, IE, IT, LV, LT, NL, NO, PL, PT, SE. Tink enforces market restriction at the Link session.
## Installation
{"fctl payments connectors install tink config.json"}
### Configuration fields
`endpoint` is `https://api.tink.com`. No separate sandbox endpoint — Tink uses test users on the same host.
## Capabilities
- `FETCH_ACCOUNTS` — depository accounts per PSU connection.
- `FETCH_BALANCES` — available + booked balance per account.
- `FETCH_EXTERNAL_ACCOUNTS` — counterparty accounts when Tink Payments identifies them.
- `FETCH_PAYMENTS` — transactions, paginated via `pageToken`.
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — webhooks per PSU user for account-data-refreshed and consent-expiry events.
The connector doesn't initiate transfers or payouts — Tink Payments (the outbound product) isn't wired here.
## Account model
Every Payments internal account is one Tink `Account` (a depository account on a linked connection). The `reference` is the Tink account ID; `name` is the account name; `defaultAsset` is null — Tink's account payload doesn't surface currency at the account level reliably, so balance assets are inferred per-cycle via `FETCH_BALANCES`. Accounts are PSU-scoped — `psuID` tracks the PSU and `openBankingConnectionID` is set when Tink's webhook payload carries the connection ID. EXTERNAL accounts are emitted when Tink Payments identifies them. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Linking a user
Tink's auth ceremony runs through Tink Link:
1. **Create a PSU** — `v3CreatePaymentServiceUser` returns a PSU ID.
2. **Forward to Tink** — `v3ForwardPaymentServiceUserToProvider` creates a Tink user and stores the `user_id` as PSU metadata.
3. **Create a Link session** — `v3CreateLinkForPaymentServiceUser` returns a `https://link.tink.com/1.0/transactions/...` URL + `attemptID`.
4. **Frontend redirect** — the user picks a bank, completes SCA, and lands on your `clientRedirectURL` with an authorization code on the query string.
5. **Refresh-finished webhook** — the connector exchanges the code for a `refresh_token` and binds the connection.
Step-by-step walkthrough on the [Open Banking Getting Started guide](/modules/payments/connectors/open-banking/getting-started).
### Redirect URL requirements
- **HTTPS** in production.
- **Registered** under your Tink client's allowed redirect URIs.
- Mobile apps follow Tink's [Android](https://docs.tink.com/resources/transactions/optimize-your-transactions-integration) and [iOS](https://docs.tink.com/resources/transactions/optimize-your-transactions-integration) optimization guides for the in-app browser flow.
## Asset model
Multi-currency, formatted to UMN at ISO 4217 precision. Amounts are already in minor units.
## Status mapping
Tink transactions arrive in three lifecycle states:
| Tink `status` | Payment `status` |
| --- | --- |
| `PENDING`, `UNDEFINED` | `PENDING` |
| `BOOKED` | `SUCCEEDED` |
| anything else | `UNKNOWN` |
When a `PENDING` transaction matures to `BOOKED`, Tink replaces the provisional ID with a permanent one — the connector swaps the reference and moves the row to `SUCCEEDED`.
## Metadata keys
Under `com.tink.spec/`:
- **Account**: `account_id`, `name`, `iban`, `bic`, `account_number`, `holder_name`, `type` (`CHECKING` / `SAVINGS` / `CREDIT_CARD` / …), `flags`.
- **External account**: `counterparty_name`, `counterparty_account_number`, `counterparty_iban`.
- **Payment**: `transaction_id`, `provider_transaction_id`, `merchant_category_code`, `description`, `payee_message`, `payer_message`, `transaction_code`.
PSU-level metadata carries `user_id` — the Tink user the connector created.
## Workflow tree
```text
FetchAccounts (periodic) — per PSU
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per PSU/account, pageToken cursor
FetchExternalAccounts (periodic)
CreateWebhooks — provisioned automatically on link
```
## Pagination and recovery
`/data/v2/transactions` uses `pageToken`-based pagination. The connector persists the cursor per PSU/account in platform-managed `State`; restarts resume from the last committed cursor.
## Known gaps
- **Tink Payments** (outbound initiation) is not wired.
- **Identity** and **Income** products aren't surfaced.
- **Consent expiry**: connections expire after the PSD2 SCA window (~90 days). Tink emits `consent_expired`; the connector translates it to a `UserConnectionPendingDisconnect` → `UserConnectionDisconnected` pair. Trigger a fresh link via `v3UpdateLinkForPaymentServiceUser` to renew.
---
## Reference
Source: https://docs.formance.com/modules/numscript/reference
## Numscript Reference
Language reference for Numscript — the transaction scripting language used by the Formance Ledger.
---
## Powens
Source: https://docs.formance.com/modules/payments/connectors/open-banking/powens
The Powens connector links Formance to Powens' European bank-aggregation platform. It drives the [Open Banking PSU flow](/modules/payments/connectors/open-banking/getting-started) across Powens' markets: create a Payment Service User, forward them to Powens, hand them a Powens Webview session, and sync the resulting accounts, balances, and transactions back through Payments. Available from Payments 3.2.0.
The Powens connector requires Payments **3.2.0 or higher**. Your stack pins an older version — upgrade to use it.
## Prerequisites
You need a Powens domain (`*.biapi.pro`) and a `clientID` + `clientSecret` + `configurationToken` triplet. Each PSU is authenticated by a user-scoped permanent access token derived from the configurationToken at registration; the connector mints and stores the token per PSU automatically.
`maxConnectionsPerLink` caps the number of bank connections per Webview session — Powens lets you raise the cap; pick the depth of integration you want.
## Installation
{"fctl payments connectors install powens config.json"}
### Configuration fields
`domain` is the tenant-scoped subdomain Powens issued (e.g. `acme.biapi.pro`). `endpoint` is the API root for that domain, typically `https:///2.0`.
## Capabilities
- `FETCH_ACCOUNTS` — depository, credit, and loan accounts per PSU connection.
- `FETCH_BALANCES` — available + booked balance per account.
- `FETCH_EXTERNAL_ACCOUNTS` — counterparty accounts when Powens identifies them.
- `FETCH_PAYMENTS` — transactions, paginated via `limit` + `offset`.
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — subscribes to `USER_SYNCED`, `CONNECTION_SYNCED`, `CONNECTION_DELETED`, ….
The connector doesn't initiate transfers or payouts — Powens' outbound product is not wired here.
## Account model
Every Payments internal account is one Powens `BankAccount` from a linked connection. The `reference` is the account `ID` stringified; `name` is the `original_name`; `defaultAsset` is `currency.id` at the precision Powens publishes. Accounts are PSU-scoped — `psuID` and `openBankingConnectionID` track the link. When Powens reports an `error` on the account, it lands in metadata. EXTERNAL accounts are emitted when Powens identifies them. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Linking a user
Powens' auth ceremony runs through Powens Webview:
1. **Create a PSU** — `v3CreatePaymentServiceUser` returns a PSU ID.
2. **Forward to Powens** — `v3ForwardPaymentServiceUserToProvider` creates a Powens user with the `configurationToken`, mints a permanent access token, and stores both as PSU metadata.
3. **Create a Webview session** — `v3CreateLinkForPaymentServiceUser` returns a `https://webview.powens.com/...` URL + `attemptID`.
4. **Frontend redirect** — the user picks a bank, completes SCA, and lands on your `clientRedirectURL`.
5. **`USER_SYNCED` webhook** — the connector ingests it and starts syncing the linked connections.
Step-by-step walkthrough on the [Open Banking Getting Started guide](/modules/payments/connectors/open-banking/getting-started).
### Redirect URL requirements
- **HTTPS** in production.
- **Registered** under the Powens client's allowed redirect URIs.
- Mobile apps must follow Powens' [Webview browser-integration guide](https://docs.powens.com/api-reference/overview/webview#browser-integration) — in-app webviews aren't supported by every bank in the network.
## Asset model
Multi-currency, formatted to UMN at ISO 4217 precision. Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
Powens transactions are non-stateful; they appear after they post:
| Powens `coming` flag | Payment `status` |
| --- | --- |
| `true` (provisional / pending settlement) | `PENDING` |
| `false` (posted) | `SUCCEEDED` |
When a `PENDING` transaction matures, Powens replaces the provisional ID with a permanent one and the connector swaps the reference.
## Metadata keys
Under `com.powens.spec/`:
- **Account**: `account_id`, `iban`, `bic`, `account_number`, `name`, `type` (`checking` / `savings` / `card` / `loan` / …), `usage` (`PRIV` / `ORGA`).
- **External account**: `counterparty_id`, `account_number`, `iban`, `name`.
- **Payment**: `transaction_id`, `category_id`, `description`, `original_wording`, `simplified_wording`, `last_update`, `type` (`transfer` / `card` / `bank` / …).
PSU-level metadata carries `user_id` (the Powens user) and `expires_in` (token remaining lifetime).
## Workflow tree
```text
FetchAccounts (periodic) — per PSU
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per PSU/account, offset/limit cursor
FetchExternalAccounts (periodic)
CreateWebhooks — provisioned at install
```
## Pagination and recovery
`limit` + `offset`. Watermarks persist per PSU/account in platform-managed `State`; restarts resume from the last committed offset.
## Known gaps
- **Outbound initiation** is not wired.
- **Investment accounts** surface as accounts but their positions don't — only depository balances and transactions land.
- **Consent expiry**: connections expire after the PSD2 SCA window. Powens emits `CONNECTION_PENDING_DISCONNECT`; the connector translates it to `UserConnectionPendingDisconnect` — trigger a fresh Webview via `v3UpdateLinkForPaymentServiceUser` to renew.
---
## Connectors
Source: https://docs.formance.com/modules/payments/connectors
Formance Payments supports three categories of connectors for integrating with financial service providers.
**PSP Connectors** integrate with traditional payment service providers like Stripe, Adyen, and Wise — syncing payments, accounts, and balances into Formance.
**Exchange Connectors** integrate with crypto and asset exchanges like Coinbase Prime and Fireblocks — syncing wallets, balances, trading orders, and conversions alongside payments.
**Open Banking** connectors link to aggregators like Plaid, Tink, and Powens — enabling end users to securely connect their bank accounts and share account data.
## Edition
Connectors split across the two Formance editions. The standard PSP connectors and every Open Banking connector ship in the **Community edition**. The Exchange connectors (Coinbase Prime, Fireblocks, Bitstamp) and the two EE PSPs (Routable, Banking Bridge) are part of the **Enterprise edition** and require an EE license to install. The [Capabilities](/modules/payments/capabilities) page lists every connector's edition alongside its supported capabilities — the matrix is derived from upstream code at build time, so it stays in sync with what actually ships in each Payments minor.
---
## How it Works
Source: https://docs.formance.com/modules/payments/connectors/generic/how-it-works
The Generic Connector for Formance Payments provides a way to connect your Formance Stack with Financial Service Providers that are not natively supported by Formance.
Formance Payments interacts with the remote Financial Service Provider through the Generic Connector as follows:
```mermaid
sequenceDiagram
Payments ->> Generic Connector: Poll the data
activate Generic Connector
Generic Connector -->> Payments: Ok
par Account list
Generic Connector ->> PSP: Request account list
PSP -->> Generic Connector: Account list
loop for each account
Generic Connector ->> PSP: Request account balance
PSP -->> Generic Connector: Account balance
end
Generic Connector -->> Payments: Account list
Generic Connector ->> PSP: Request account transactions
PSP -->> Generic Connector: Account transactions
Generic Connector -->> Payments: Transactions list
and Beneficiary list
Generic Connector ->> PSP: Request beneficiary list
PSP -->> Generic Connector: Beneficiary list
Generic Connector -->> Payments: Beneficiary list
end
deactivate Generic Connector
```
The Generic Connector is in charge of polling the data from the Financial Service Provider and then sending it to Formance Payments. It polls the following data:
- The list of accounts available in the Financial Service Provider and their associated balances and transactions
- The list of beneficiaries available in the Financial Service Provider for payouts
## Integration with the Financial Service Provider
The Generic Connector interacts with the Financial Service Provider by sending requests formatted according to the contract defined in [`generic-openapi.yaml`](https://github.com/formancehq/payments/blob/main/internal/connectors/plugins/public/generic/client/generic-openapi.yaml) and expecting responses formatted according to the same contract.
As a consequence, it is necessary to create a service on your side that will interact with the Financial Service Provider and expose the data in the expected format.
### Endpoints your service must expose
The connector calls six endpoints on the service URL you configured at install time. Every endpoint is mounted at the root and authenticates via the `Authorization: Bearer ` header.
| Method | Path | Purpose | Payments capability |
| --- | --- | --- | --- |
| `GET` | `/accounts?pageSize=&page=&sort=&createdAtFrom=` | List internal accounts. | `FETCH_ACCOUNTS` |
| `GET` | `/accounts/{accountId}/balances` | Return point-in-time balance(s) for an internal account. | `FETCH_BALANCES` |
| `GET` | `/beneficiaries?pageSize=&page=&sort=&createdAtFrom=` | List external accounts (beneficiaries). | `FETCH_EXTERNAL_ACCOUNTS` |
| `GET` | `/transactions?pageSize=&page=&sort=&updatedAtFrom=` | List transactions updated after `updatedAtFrom`. | `FETCH_PAYMENTS` |
| `POST` | `/payouts` | Initiate a payout to a beneficiary. Idempotent via `idempotencyKey` on the body. | `CREATE_PAYOUT` |
| `POST` | `/transfers` | Initiate a transfer between two internal accounts. Idempotent via `idempotencyKey` on the body. | `CREATE_TRANSFER` |
### Required behaviors
- **Pagination** — the list endpoints (`/accounts`, `/beneficiaries`, `/transactions`) accept `pageSize` and `page` query parameters. `pageSize` defaults to 100; honour it. The connector advances `page` until the response returns fewer than `pageSize` items, at which point it considers the cycle complete.
- **Incremental sync** — `/transactions` carries an `updatedAtFrom` query parameter that the connector seeds with the latest watermark from the previous cycle. Return only transactions updated at or after that timestamp. The connector dedupes by the `id` field on the response, so re-emitting the boundary row is safe (and recommended — it covers clock-skew edges).
- **Idempotency** — `POST /payouts` and `POST /transfers` both carry an `idempotencyKey` on the request body. If your service sees the same key twice, return the original response (HTTP 201) rather than initiating a duplicate.
- **Error envelope** — failures return any non-`2xx` status with a JSON body shaped as `{"errorCode": "...", "errorMessage": "..."}`. The connector surfaces both fields in the resulting Payment adjustment.
The full OpenAPI document for the contract lives at
[`internal/connectors/plugins/public/generic/client/generic-openapi.yaml`](https://github.com/formancehq/payments/blob/main/internal/connectors/plugins/public/generic/client/generic-openapi.yaml).
That's the source of truth — fields you don't see on this page are still defined there (request bodies, response schemas, enum values).
The typical deployment of the Generic Connector is as follows:
```mermaid
graph LR
Payments["Payments Core"]
connector["Generic Connector"]
service["Integration Service"]
psp["Financial Service Provider"]
subgraph Formance Payments
Payments <--> connector
end
subgraph Your infrastructure
connector <-- HTTP --> service
end
service <-- PSP specific protocol --> psp
```
## Polling mechanism
The Generic Connector uses a state-based approach for polling payment data efficiently.
### How it works
1. The connector stores the timestamp of the last successful data retrieval
2. In subsequent polls, it uses this timestamp to fetch only new or updated data
3. The `UpdatedAtFrom` query parameter is passed in API calls to your service
4. This parameter indicates that only transactions from that specific point in time should be returned
### Data storage and updates
The system doesn't fetch all data fresh every time it polls. Instead:
- Each batch of data received is stored
- The internal state is updated with the latest timestamp of the data received
- In the next polling cycle, the updated timestamp is used to fetch only new or changed data
### Benefits
This polling method offers several advantages:
- **Reduced data transfer**: Only fetches new or updated information
- **Minimized load**: Reduces strain on both Formance and your API
- **No duplicates**: Ensures the database stays up-to-date without duplicating existing data
When setting up the Generic Connector, ensure that your API can handle and respond correctly to the `UpdatedAtFrom` query parameter. This allows the system to efficiently retrieve only the necessary data during each polling cycle.
## Authentication
When instantiating the Generic Connector, you will need to pass an API key that will be used to authenticate the requests to your service. The Generic Connector will send requests with the API key in the `Authorization` header so that your service can authenticate the requests.
Example:
```
Authorization: Bearer
```
---
## Adyen
Source: https://docs.formance.com/modules/payments/connectors/psp/adyen
The Adyen connector syncs merchant accounts from Adyen's Management API and ingests Adyen's webhook stream. It does not poll for payments or balances — Adyen's canonical transaction surface is the webhook feed, which the connector subscribes to at install time.
## Prerequisites
You need an Adyen Management API key with read access to `MerchantAccount` for the company. Webhooks use Basic Auth credentials (`webhookUsername` / `webhookPassword`) set on the webhook config in Adyen's Customer Area and sent as `Authorization: Basic ` on every delivery.
## Installation
{"fctl payments connectors install adyen config.json"}
### Configuration fields
`liveEndpointPrefix` only applies in production — copy it from Adyen's Customer Area under **Developers → API URLs** (the company-specific fragment between `https://` and `-checkout-live.adyenpayments.com`). Leave it empty for the test environment.
## Capabilities
- `FETCH_ACCOUNTS` — merchant accounts via `GET /Management/v3/merchants`.
- `CREATE_WEBHOOKS` — provisions a "Standard webhook" on the merchant at install, configured with the Basic Auth credentials from the config.
- `TRANSLATE_WEBHOOKS` — converts `AUTHORISATION`, `CAPTURE`, `REFUND`, `CHARGEBACK`, `CANCELLATION`, etc. to `PSPPayment`s.
Payments, balances, and external accounts are deliberately not polled — Adyen's canonical transaction surface is webhooks. Ensure the webhook is reachable (HTTPS, valid certificate) and the Basic Auth credentials match the config.
## Account model
Every Payments internal account is one Adyen **merchant account** from `GET /Management/v3/merchants`. The `reference` is the merchant ID; `name` is the display name; `defaultAsset` is null (Adyen merchants are multi-currency). No EXTERNAL accounts are emitted — Adyen "destination accounts" are not pulled (see [Known gaps](#known-gaps)). See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Currencies are uppercase ISO 4217 (`USD`, `EUR`, `JPY`), formatted to UMN at standard precision. Amounts are already in minor units — no scaling.
## Status mapping
Webhook events combine `success: true|false` with `eventCode`:
| Adyen `eventCode` + `success` | Payment `status` |
| --- | --- |
| `AUTHORISATION` + `true` | `PENDING` (until capture) |
| `CAPTURE` + `true`, `REFUND` + `true` | `SUCCEEDED` |
| `AUTHORISATION` + `false`, `REFUND_FAILED`, `CHARGEBACK` | `FAILED` |
| `CANCELLATION` + `true` | `CANCELLED` |
Full event-code → type/status mapping in [`webhooks.go`](https://github.com/formancehq/payments/blob/main/internal/connectors/plugins/public/adyen/webhooks.go).
## Metadata keys
Under `com.adyen.spec/`:
- **Account**: `merchant_account_code`, `description`, `data_centers`.
- **Payment**: `event_code`, `psp_reference`, `merchant_reference`, `payment_method`, `payment_method_variant`, `acquirer_code`.
## Workflow tree
```text
FetchAccounts (periodic — merchant accounts only)
CreateWebhooks (one-shot at install)
```
## Known gaps
- **Balances and payouts** are not exposed — Adyen's payout flow is bank-side, outside the Management API.
- **External accounts** (Adyen "destination accounts") are not pulled.
- **Transfer / Payout Initiation** is not implemented; outbound flows live in Adyen's Customer Area.
---
## Atlar
Source: https://docs.formance.com/modules/payments/connectors/psp/atlar
The Atlar connector polls an Atlar workspace and surfaces accounts, counterparties (external accounts), and transactions. Atlar's transactional model is bank-rail flat (SEPA, BACS, ACH), so the connector uses one shared shape across rails.
## Prerequisites
You need an Atlar workspace and a pair of HMAC credentials (`accessKey` + `secret`). Atlar signs every request with HMAC-SHA256; the connector signs internally — you only supply the credentials.
## Installation
{"fctl payments connectors install atlar config.json"}
### Configuration fields
`baseUrl` defaults to `https://api.atlar.com`; override only when Atlar provisions you against a dedicated sandbox or staging URL.
## Capabilities
- `FETCH_ACCOUNTS` — internal accounts via `/v1/accounts`.
- `FETCH_EXTERNAL_ACCOUNTS` — counterparties via `/v1/counterparties`.
- `FETCH_PAYMENTS` — `/v1/transactions`, classified as PAY-IN / PAYOUT / TRANSFER by direction.
- `FETCH_OTHERS` — `external-payments` (incoming SCT/SDD instructions awaiting reconciliation), surfaced under the `external-payment` collection on `v3ListPaymentsOther`.
Outbound transfer/payout initiation, bank-account creation, and webhooks are not exposed on the Atlar API surface this connector reaches.
## Account model
Every Payments internal account is one Atlar `/v1/accounts` row. The `reference` is the Atlar account `ID`; `name` is the account name; `defaultAsset` is the account currency at ISO 4217 precision. EXTERNAL accounts come from `/v1/counterparties`. Metadata stamps the underlying `bank/id`, `bank/name`, `bank/bic`, IBAN and other identifiers per market+type, plus `alias`, `owner/name`, and the `fictive` flag. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Uppercase ISO 4217 (`EUR`, `GBP`, `USD`), formatted to UMN at standard precision. Amounts are already in minor units — no scaling.
## Status mapping
| Atlar transaction `status` | Payment `status` |
| --- | --- |
| `INITIATED`, `PENDING`, `SUBMITTED`, `INFLIGHT` | `PENDING` |
| `EXECUTED`, `REGISTERED`, `RECONCILED` | `SUCCEEDED` |
| `FAILED`, `REJECTED` | `FAILED` |
| `CANCELLED` | `CANCELLED` |
| anything else | `UNKNOWN` |
## Metadata keys
Under `com.atlar.spec/`:
- **Account**: `account_id`, `bank_iban`, `bank_bic`, `account_type`, `currency_code`.
- **External account**: `counterparty_id`, `name`, `account_number`, `routing_number`, `bic`, `country`.
- **Payment**: `transaction_id`, `transaction_type`, `bank_reference`, `remittance_information_unstructured`, `direction`.
## Workflow tree
```text
FetchAccounts (periodic)
└── FetchPayments (periodic) — per account
FetchExternalAccounts (periodic)
FetchOthers (periodic) — external-payments awaiting reconciliation
```
## Known gaps
- **Balances** are not exposed — Atlar's `/v1/accounts` payload doesn't carry a real-time balance.
- **Outbound initiation** (`CreateTransfer` / `CreatePayout`) is not implemented; Atlar's payment-initiation API requires payment-plan scaffolding without a Payments-side model.
- **Webhooks** are not wired; reconciliation runs on the polling cycle.
---
## Banking Circle
Source: https://docs.formance.com/modules/payments/connectors/psp/bankingcircle
The Banking Circle connector polls a Banking Circle tenant and surfaces settlement accounts, balances, and transactions. It also initiates transfers and payouts, and registers new beneficiaries on Banking Circle's side.
## Prerequisites
You need a Banking Circle tenant and a mutual-TLS-capable API user. Authentication combines a username/password pair (for OAuth2) with a client certificate (`userCertificate` + `userCertificateKey`) on every call. The cert is PEM-encoded; supply certificate and key as multi-line strings in the config.
Banking Circle exposes distinct base URLs for the authorization endpoint (`https://authorizationsandbox.bankingcircleconnect.com` in sandbox) and the data endpoint (`https://sandbox.bankingcircleconnect.com`). Both are required.
## Installation
{"fctl payments connectors install bankingcircle config.json"}
### Configuration fields
## Capabilities
- `FETCH_ACCOUNTS` — settlement and customer accounts via `GET /api/v1/accounts`.
- `FETCH_BALANCES` — available + intraday balance per account.
- `FETCH_PAYMENTS` — transactions, classified PAY-IN / PAYOUT / TRANSFER by direction.
- `CREATE_BANK_ACCOUNT` — registers a beneficiary from a `v3CreateBankAccount` call.
- `CREATE_TRANSFER` — `POST /api/v1/payments/singles` between two Banking Circle accounts.
- `CREATE_PAYOUT` — `POST /api/v1/payments/singles` to a registered beneficiary.
Webhooks are not exposed on the API surface this connector reaches; refresh runs on the polling cycle.
## Account model
Every Payments internal account is one Banking Circle account from `/api/v1/accounts` — typically a settlement or customer ledger. The `reference` is the Banking Circle `accountId`; `name` is the description; `defaultAsset` is the account's `currency`. EXTERNAL accounts aren't pulled, but `v3CreateBankAccount` (via `CREATE_BANK_ACCOUNT`) registers a beneficiary on Banking Circle and links the result back. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Uppercase ISO 4217 (`EUR`, `GBP`, `USD`, …), formatted to UMN at standard precision. Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
| Banking Circle transaction `status` | Payment `status` |
| --- | --- |
| `Pending`, `Processed` (booked but not settled) | `PENDING` |
| `Settled`, `Completed` | `SUCCEEDED` |
| `Failed`, `Rejected` | `FAILED` |
| `Cancelled` | `CANCELLED` |
`Create*` initiations schedule `PollTransferStatus` / `PollPayoutStatus` against the singles-payment endpoint until terminal.
## Metadata keys
Under `com.bankingcircle.spec/`:
- **Account**: `account_id`, `iban`, `bic`, `currency_code`, `account_type`.
- **Payment**: `transaction_id`, `transaction_reference`, `end_to_end_id`, `payment_method`, `clearing_system`.
- **External account**: `beneficiary_id`, `country`, `iban`, `bic`, `routing_number`.
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (periodic) — per account
└── FetchPayments (periodic) — per account
CreateBankAccount / CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
```
## Pagination and recovery
Banking Circle transactions are `pageNumber` + `pageSize`. The connector persists the latest watermark per account in platform-managed `State` and re-fetches one cycle back on restart, deduping by `PSPPayment.Reference`.
## Known gaps
- **Webhooks** are not wired.
- **Reversals** (`ReverseTransfer` / `ReversePayout`) are not implemented; corrections live in Banking Circle's UI.
- **External accounts via FETCH** are not pulled — beneficiaries are managed via `CreateBankAccount` and persisted Payments-side.
---
## Coinbase Prime
Source: https://docs.formance.com/modules/payments/connectors/exchange/coinbaseprime
The Coinbase Prime connector polls a Coinbase Prime portfolio and surfaces its wallets, balances, payments, trading orders, and conversions to the Payments service. It is read-only.
## Prerequisites
The Coinbase Prime connector requires Payments module **3.2.0 or higher** for Accounts, Balances, and Payments. Your stack pins an older version — upgrade to use it.
Your stack supports Accounts, Balances, and Payments via Coinbase Prime. **Orders and Conversions** require Payments module 3.3.0 or higher.
You need a Coinbase Prime account and an API key with the minimum permissions for the capabilities you use.
To create an API key in the Coinbase Prime console:
Go to **Settings**, then **API**.
Click **Create API Key** and select the portfolio you want to connect.
Choose an expiration date as needed.
If you set an expiration date, you will need to update the connector configuration with new credentials when the key expires.
Set **Read** permissions — the connector is read-only and polls Accounts, Balances, Payments, Orders, and Conversions. Configure IP restrictions if your environment requires them.
Save the **API key**, **API secret**, and **passphrase** — all three are required for the connector configuration.
## Installation
{"fctl payments connectors install coinbaseprime config.json"}
With `config.json` containing:
```json
{
"apiKey": "string",
"apiSecret": "string",
"name": "string",
"passphrase": "string",
"pollingPeriod": "30m",
"portfolioId": "string"
}
```
### Configuration fields
| Field | Required | Default | Description |
|---|---|---|---|
| `apiKey` | yes | — | Coinbase Prime API key. |
| `apiSecret` | yes | — | Secret key from API-key creation. |
| `name` | yes | — | Unique name for this connector instance. |
| `passphrase` | yes | — | Passphrase from API-key creation. |
| `pollingPeriod` | no | `30m` | Sync cadence. |
| `portfolioId` | yes | — | Portfolio to connect. One connector per portfolio — install several to track more than one. |
## Capabilities
- `FETCH_ACCOUNTS` — portfolio wallets.
- `FETCH_BALANCES` — per-wallet balances.
- `FETCH_PAYMENTS` — deposits, withdrawals, internal transfers, rewards, and staking flows.
- `FETCH_ORDERS` — spot trading orders with full fill lifecycle. Requires Payments 3.3.0+.
- `FETCH_CONVERSIONS` — atomic two-asset swaps (e.g. stablecoin redemption). Requires Payments 3.3.0+.
Outbound initiation, webhooks, and external-account creation are not implemented — see [Known gaps](#known-gaps).
## Account model
Every Payments internal account is one Coinbase Prime wallet in the configured portfolio. Coinbase Prime distinguishes wallet types — trading, vault, onchain, custody (`QC`), and `WALLET_TYPE_OTHER` — and only **trading wallets** settle orders. Order resolution is therefore restricted to trading wallets so each asset symbol resolves unambiguously.
The `reference` is the `walletID`; `defaultAsset` is the wallet's symbol (e.g. `BTC/8`, `USD/2`). No EXTERNAL accounts are emitted — counterparty addresses surface in Payment metadata only. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Payments
Coinbase Prime transactions land on the [Payments](/modules/payments/payments) stream, **excluding** rows of type `CONVERSION` (which feed [Conversions](#conversions)). Each upstream transaction lands on exactly one stream.
### Transaction type to Payment type
| Coinbase transaction type | Payment `type` |
| --- | --- |
| `DEPOSIT`, `COINBASE_DEPOSIT`, `COINBASE_REFUND`, `REWARD`, `DEPOSIT_ADJUSTMENT`, `CLAIM_REWARDS` | `PAY-IN` |
| `WITHDRAWAL`, `SWEEP_WITHDRAWAL`, `PROXY_WITHDRAWAL`, `BILLING_WITHDRAWAL`, `WITHDRAWAL_ADJUSTMENT`, `SLASH` | `PAYOUT` |
| `INTERNAL_DEPOSIT`, `INTERNAL_WITHDRAWAL`, `SWEEP_DEPOSIT`, `PROXY_DEPOSIT`, `STAKE`, `RESTAKE`, `PORTFOLIO_STAKE`, `UNSTAKE`, `PORTFOLIO_UNSTAKE` | `TRANSFER` |
| Chain-level events (`KEY_REGISTRATION`, `DELEGATION`, `VOTE_AUTHORIZE`, `ONCHAIN_TRANSACTION`, …) and any unrecognized type | `OTHER` |
### Status mapping
Successful transactions → `SUCCEEDED`; failures and rejections → `FAILED`; mid-flight → `PENDING`. Cancellations and expirations map distinctly for dashboarding.
| Coinbase transaction status | Payment `status` |
| --- | --- |
| `TRANSACTION_PENDING`, `TRANSACTION_CREATED`, `TRANSACTION_REQUESTED`, `TRANSACTION_APPROVED`, `TRANSACTION_GASSING`, `TRANSACTION_GASSED`, `TRANSACTION_PROVISIONED`, `TRANSACTION_PLANNED`, `TRANSACTION_PROCESSING`, `TRANSACTION_RESTORED`, `TRANSACTION_IMPORT_PENDING`, `TRANSACTION_DELAYED`, `TRANSACTION_BROADCASTING`, `TRANSACTION_CONSTRUCTED` | `PENDING` |
| `TRANSACTION_DONE`, `TRANSACTION_IMPORTED` | `SUCCEEDED` |
| `TRANSACTION_CANCELLED` | `CANCELLED` |
| `TRANSACTION_EXPIRED` | `EXPIRED` |
| `TRANSACTION_FAILED`, `TRANSACTION_REJECTED` | `FAILED` |
| `OTHER_TRANSACTION_STATUS` | `OTHER` |
| anything else | `UNKNOWN` |
### Amount and asset
Amounts use the precision from Coinbase Prime's asset catalogue (`GetAssets` provides `decimal_precision` per symbol). Common assets: `BTC/8`, `ETH/18`, `USDC/6`, `USD/2`. Always trust the `asset` field on the Payment rather than assuming a precision.
Transactions referencing an asset not in the catalogue are skipped (logged) and don't produce a Payment.
### Account resolution
Each leg resolves independently:
1. If Coinbase populates `transfer_from.value` (or `transfer_to.value`) with `type == WALLET`, that wallet ID becomes the leg's account reference.
2. Otherwise the connector falls back to the transaction's `wallet_id`: `PAY-IN` → destination, `PAYOUT` → source.
3. External blockchain addresses (e.g. self-custody withdrawal) land in Payment metadata as `source_address` / `deposit_address`, never as account references.
### Metadata
Under `com.formance.connectors.coinbaseprime.`:
- **Always present**: `type`, `status`.
- **Present when populated**: `wallet_id`, `portfolio_id`, `network`, `external_tx_id`, `source_address`, `deposit_address`, `completed_at`, `blockchain_ids`.
- **Present when any fee is non-zero**: `fees`, `network_fees` (only non-zero), `fee_symbol`.
### Example response
A 1.5 ETH withdrawal from a trading wallet to an external address:
```json
{
"id": "",
"connectorID": "",
"provider": "coinbaseprime",
"reference": "tx_4f3a8e9d1c",
"createdAt": "2026-04-30T08:14:22Z",
"type": "PAYOUT",
"amount": 1500000000000000000,
"initialAmount": 1500000000000000000,
"asset": "ETH/18",
"scheme": "OTHER",
"status": "SUCCEEDED",
"sourceAccountID": "",
"destinationAccountID": null,
"metadata": {
"com.formance.connectors.coinbaseprime.type": "WITHDRAWAL",
"com.formance.connectors.coinbaseprime.status": "TRANSACTION_DONE",
"com.formance.connectors.coinbaseprime.wallet_id": "wlt_eth_abc123",
"com.formance.connectors.coinbaseprime.deposit_address": "0xabc1234567890def...",
"com.formance.connectors.coinbaseprime.fees": "0.0021",
"com.formance.connectors.coinbaseprime.fee_symbol": "ETH"
}
}
```
`destinationAccountID` is `null` because the destination is an external blockchain address, preserved in `metadata.deposit_address`.
## Orders
Orders require Payments module 3.3.0 or higher.
Coinbase Prime trading orders land on the [Orders](/modules/payments/orders) stream — one record per Coinbase order with an append-only `adjustments` list capturing each fill or status change.
### Direction and account legs
| Direction | `sourceAsset` | `destinationAsset` | `sourceAccountID` | `destinationAccountID` |
| --- | --- | --- | --- | --- |
| `BUY` | quote | base | quote-currency trading wallet | base-currency trading wallet |
| `SELL` | base | quote | base-currency trading wallet | quote-currency trading wallet |
Both legs are restricted to **trading** wallets — vault, onchain, and custody are ineligible. If the trading wallet for a symbol hasn't been synced yet, the order page retries on the next cycle (no row is dropped).
### Status mapping
Coinbase Prime statuses map directly to Payments statuses, with one exception: `PARTIALLY_FILLED` is computed locally — Coinbase doesn't expose it. An order becomes `PARTIALLY_FILLED` when the upstream status is `OPEN` and the filled quantity sits strictly between zero and the ordered quantity.
| Coinbase status | Order `status` |
| --- | --- |
| `PENDING` | `PENDING` |
| `OPEN` with no fills | `OPEN` |
| `OPEN` with `0 < filled_quantity < base_quantity` | `PARTIALLY_FILLED` (computed) |
| `FILLED` | `FILLED` |
| `CANCELLED` | `CANCELLED` |
| `EXPIRED` | `EXPIRED` |
| `FAILED` | `FAILED` |
### Quantities, amounts, and prices
| Coinbase field | Order field | Precision |
| --- | --- | --- |
| `base_quantity` | `baseQuantityOrdered` | base asset |
| `filled_quantity` | `baseQuantityFilled` | base asset |
| `filled_value` | `quoteAmount` | quote asset |
| `commission` | `fee` (with `feeAsset` = quote) | quote asset |
Coinbase Prime returns price strings derived from `float64`, so values like `"1825.6099999998417653"` carry float-encoding noise past roughly 10 decimals. The connector picks a **dynamic precision** equal to the maximum number of decimals across `limit_price`, `stop_price`, and `average_filled_price`, capped at 10, and parses prices with a truncating decimal parser. `priceAsset` may therefore declare a higher precision than `quoteAsset` (e.g. `quoteAsset = USD/2`, `priceAsset = USD/6`).
### Metadata
Under `com.formance.connectors.coinbaseprime.`:
`product_id`, `portfolio_id`, `client_order_id`, `quote_value`, `filled_value`, `order_total`, `exchange_fee`, `net_average_filled_price`, `historical_pov`, `quote_currency`, `price_asset`, `base_wallet_id`, `quote_wallet_id`, optional `post_only`. Commission breakdown adds `commission_total`, `commission_client`, `commission_venue`, `commission_ces`, `commission_financing`, `commission_regulatory`, `commission_clearing` when Coinbase returns the detail block.
### Example response
A partially-then-fully-filled `BUY` of 0.5 BTC on `BTC-USD` at a 50,000 USD limit:
```json
{
"id": "",
"reference": "ord_9c7e1a4b3d",
"direction": "BUY",
"sourceAsset": "USD/2",
"destinationAsset": "BTC/8",
"type": "LIMIT",
"status": "FILLED",
"timeInForce": "GOOD_UNTIL_CANCELLED",
"baseQuantityOrdered": 50000000,
"baseQuantityFilled": 50000000,
"limitPrice": 5000000,
"averageFillPrice": 4998750,
"quoteAmount": 2499375,
"quoteAsset": "USD/2",
"priceAsset": "USD/2",
"fee": 1250,
"feeAsset": "USD/2",
"adjustments": [
{ "createdAt": "2026-04-30T09:00:05Z", "status": "PENDING", "baseQuantityFilled": 0 },
{ "createdAt": "2026-04-30T09:00:30Z", "status": "OPEN", "baseQuantityFilled": 0 },
{ "createdAt": "2026-04-30T09:08:45Z", "status": "PARTIALLY_FILLED", "baseQuantityFilled": 22500000, "fee": 562 },
{ "createdAt": "2026-04-30T09:12:30Z", "status": "FILLED", "baseQuantityFilled": 50000000, "fee": 1250 }
]
}
```
## Conversions
Conversions require Payments module 3.3.0 or higher.
The connector queries Coinbase Prime's transactions feed filtered to type `CONVERSION` and emits one [Conversion](/modules/payments/conversions) per row. Coinbase Prime exposes a single `amount` field per conversion, so `sourceAmount` and `destinationAmount` are populated **1:1** (parsed at each side's precision) — correct for stablecoin redemption (USDC ↔ USD) where nominal value is identical.
### Asset and account resolution
| Coinbase field | Conversion field |
| --- | --- |
| `symbol` | `sourceAsset` |
| `destination_symbol` | `destinationAsset` |
| `transfer_from.value` | `sourceAccountID` |
| `transfer_to.value` | `destinationAccountID` |
Rows with a missing symbol or one absent from the catalogue are skipped (logged). Account references are used directly without gating on `transfer_*.type`; either may be empty without blocking emission.
### Status mapping
Conversions have a tighter lifecycle than Payments — no `CANCELLED` or `EXPIRED`.
| Coinbase transaction status | Conversion `status` |
| --- | --- |
| `TRANSACTION_DONE`, `TRANSACTION_IMPORTED` | `COMPLETED` |
| `TRANSACTION_FAILED`, `TRANSACTION_REJECTED`, `TRANSACTION_CANCELLED` | `FAILED` |
| anything else | `PENDING` |
### Fees
`fees` is parsed at the precision of `fee_symbol`, falling back to the source symbol when absent. If the fee currency isn't in the catalogue, only the fee is dropped — the conversion itself is still emitted.
### Metadata
Under `com.formance.connectors.coinbaseprime.`: `transaction_id`, `type` (always present); `portfolio_id` (when populated).
### Example response
A 10,000 USDC → USD conversion (no fee on stablecoin redemption):
```json
{
"id": "",
"reference": "tx_d2b4a17e9c",
"sourceAsset": "USDC/6",
"destinationAsset": "USD/2",
"sourceAmount": 10000000000,
"destinationAmount": 1000000,
"fee": null,
"feeAsset": null,
"status": "COMPLETED",
"sourceAccountID": "",
"destinationAccountID": ""
}
```
## Troubleshooting
Orders and Conversions resolve their account legs against wallets pulled by Accounts. On a fresh install, the first Accounts cycle must complete before Order and Conversion pages succeed — they retry until then.
Verify the trading wallet has synced:
```bash
fctl payments accounts list --connector-id $CONNECTOR_ID | grep TRADING
```
If still missing after several cycles, check connector logs for an unsupported asset or a Coinbase API error.
`PENDING` covers everything other than `TRANSACTION_DONE` / `TRANSACTION_IMPORTED` (success) or the failure family. Inspect the conversion's metadata for the exact upstream status:
```bash
curl -s "$STACK/api/payments/v3/conversions/$CONVERSION_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.data.metadata'
```
Cross-reference `transaction_id` against the Coinbase Prime portal.
Amounts use the precision from Coinbase Prime's `GetAssets` (plus a small fiat fallback). Transactions referencing an asset not in the catalogue are **silently skipped** and logged — no Payment, Order, or Conversion is produced.
Look for `unsupported currency` in connector logs. The asset starts flowing once Coinbase publishes precision metadata; re-trigger an Accounts sync to refresh the catalogue.
Each capability schedule exposes its run history via the schedule APIs. See [Monitoring connector schedules](/modules/payments/operations#monitoring-connector-schedules).
## Known gaps
- **Outbound initiation** — Coinbase Prime exposes withdrawal APIs, but the connector is read-only. `CreateTransfer` and `CreatePayout` are not wired.
- **Webhooks** — not consumed; refresh runs on the polling cycle.
- **Historical Orders and Conversions** — read from the activity feed forward from the first install cycle; pre-install activity is not back-filled.
- **Cross-portfolio aggregation** — one connector per `portfolioID`. Install several to track multiple portfolios.
---
## Column
Source: https://docs.formance.com/modules/payments/connectors/psp/column
The Column connector polls a Column bank-as-a-service tenant and surfaces accounts, balances, counterparties, and payments. It covers the full US-bank rail set (ACH credit/debit, wire, book transfer) for both observation and outbound initiation, plus webhook ingest.
## Prerequisites
You need a Column tenant and an API key with read access to accounts and transfers, plus write access for the rails you initiate (`ach.create`, `wire.create`, `book.create`, `counterparty.create`).
## Installation
{"fctl payments connectors install column config.json"}
### Configuration fields
`endpoint` defaults to `https://api.column.com`. Sandbox uses the same host with sandbox-scoped keys — no separate base URL.
## Capabilities
- `FETCH_ACCOUNTS` — bank accounts via `GET /bank-accounts`.
- `FETCH_BALANCES` — available + held + locked balance per account.
- `FETCH_EXTERNAL_ACCOUNTS` — counterparties via `GET /counterparties`.
- `FETCH_PAYMENTS` — ACH, wires, book transfers, and incoming credits, unified into `PSPPayment`.
- `CREATE_BANK_ACCOUNT` — registers a counterparty via `v3CreateBankAccount`.
- `CREATE_TRANSFER` — book transfer between two Column accounts.
- `CREATE_PAYOUT` — ACH credit (`POST /transfers/ach`) or wire (`POST /transfers/wire`); rail selected via `com.column.spec/rail`.
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — provisions an endpoint at install and ingests `ach.*`, `wire.*`, `book.*` events.
## Account model
Every Payments internal account is one Column bank account from `GET /bank-accounts`. The `reference` is the Column account ID; `name` is the description; `defaultAsset` is `USD/2` (Column is USD-only on the bank-product side). Metadata stamps `type`, `bic`, `default_account_number`, `routing_number`, `is_overdraftable`, `owners`. EXTERNAL accounts come from `/counterparties`; `v3CreateBankAccount` registers new ones. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
USD-only on the bank-product side — every balance and payment is `USD/2`. Amounts are already in minor units.
## Status mapping
Column statuses vary per rail; the connector collapses them:
| Column status (ACH / wire / book) | Payment `status` |
| --- | --- |
| `initiated`, `pending`, `submitted`, `manual_review`, `holding` | `PENDING` |
| `completed`, `settled`, `posted` | `SUCCEEDED` |
| `returned`, `failed`, `rejected` | `FAILED` |
| `canceled` | `CANCELLED` |
ACH credits land `settled` after the daily ACH return window — the connector folds the window into the `PENDING → SUCCEEDED` transition.
## Metadata keys
Under `com.column.spec/`:
- **Account**: `bank_account_id`, `account_number_last4`, `routing_number`, `account_type`.
- **Counterparty (external account)**: `counterparty_id`, `account_number_last4`, `routing_number`, `account_type`, `name`.
- **Payment**: `transfer_id`, `rail` (`ach` / `wire` / `book`), `direction`, `network_response_code`, `idempotency_key`, `description`.
Outbound initiation requires the rail on the PaymentInitiation metadata:
```json
{
"metadata": {
"com.column.spec/rail": "ach",
"com.column.spec/sec_code": "PPD"
}
}
```
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (periodic) — per account
└── FetchPayments (periodic) — per account, per rail
FetchExternalAccounts (periodic)
CreateBankAccount / CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
CreateWebhooks (one-shot at install)
```
## Pagination and recovery
Cursor-based pagination via `starting_after`. The connector persists cursors per stream in platform-managed `State` and resumes from the last committed cursor on restart.
## Known gaps
- **Reverse transfer** is not implemented; ACH returns surface as new payment rows rather than reversal events.
- **International wires** are supported only insofar as Column's API accepts the destination — no extra Payments-side metadata mapping for SWIFT BIC / IBAN.
---
## Currencycloud
Source: https://docs.formance.com/modules/payments/connectors/psp/currencycloud
The Currencycloud connector polls a Currencycloud account and surfaces sub-accounts, multi-currency balances, beneficiaries, and transactions (including FX conversions). It also initiates transfers between sub-accounts and payouts to registered beneficiaries.
## Prerequisites
You need a Currencycloud account and a `loginID` + `apiKey` pair. Currencycloud uses a session token derived from those credentials; the connector manages the session and re-authenticates on expiry.
## Installation
{"fctl payments connectors install currencycloud config.json"}
### Configuration fields
`endpoint` is `https://devapi.currencycloud.com/v2/` for demo/sandbox, `https://api.currencycloud.com/v2/` for production.
## Capabilities
- `FETCH_ACCOUNTS` — sub-accounts via `GET /accounts/find`.
- `FETCH_BALANCES` — per-account, per-currency balances.
- `FETCH_EXTERNAL_ACCOUNTS` — beneficiaries via `GET /beneficiaries/find`.
- `FETCH_PAYMENTS` — transactions and conversions, classified PAY-IN / PAYOUT / TRANSFER.
- `CREATE_TRANSFER` — `POST /transfers/create` between two sub-accounts.
- `CREATE_PAYOUT` — `POST /payments/create` to a registered beneficiary.
`CREATE_BANK_ACCOUNT` is not implemented; manage beneficiaries through Currencycloud directly. Webhooks are not wired.
## Account model
Every Payments internal account is one Currencycloud sub-account (`POST /v2/accounts/find`). The `reference` is the account `id`; `name` is `account_name`; `defaultAsset` is null — sub-accounts are multi-currency, with one balance row per asset via `FETCH_BALANCES`. EXTERNAL accounts come from `/beneficiaries/find`. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Multi-currency, formatted to UMN at ISO 4217 precision. Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
| Currencycloud `status` | Payment `status` |
| --- | --- |
| `pending`, `awaiting_authorization`, `submitted` | `PENDING` |
| `completed`, `released`, `settled` | `SUCCEEDED` |
| `failed`, `deleted` (reversed) | `FAILED` |
| `cancelled`, `cancellation_requested` | `CANCELLED` |
`CREATE_TRANSFER` and `CREATE_PAYOUT` schedule `PollTransferStatus` / `PollPayoutStatus` against `/transfers/{id}` or `/payments/{id}` until terminal.
## Metadata keys
Under `com.currencycloud.spec/`:
- **Account**: `account_id`, `account_name`, `legal_entity_type`, `your_reference`, `status`.
- **External account**: `beneficiary_id`, `beneficiary_country`, `currency`, `account_number`, `iban`, `bic_swift`, `routing_code_value_1`.
- **Payment**: `transaction_id`, `payment_id`, `transfer_id`, `conversion_id` (for FX legs), `payment_type` (`regular` / `priority`), `reason`.
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (periodic) — per account
└── FetchPayments (periodic) — per account
FetchExternalAccounts (periodic)
CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
```
## Pagination and recovery
1-indexed `page` + `per_page`. The connector persists the watermark per stream in platform-managed `State`; restarts resume from the last committed page boundary.
## Known gaps
- **Webhooks** are not implemented.
- **Beneficiary creation** through the Payments module is not wired — beneficiaries must be created upstream before payouts can target them.
- **FX conversions** surface as `PSPPayment`s with `com.currencycloud.spec/conversion_id`, not as separate [Conversion](/modules/payments/conversions) entries (the Conversion resource is exchange-flavoured and gated to 3.3.0+).
---
## Fireblocks
Source: https://docs.formance.com/modules/payments/connectors/exchange/fireblocks
The Fireblocks connector polls a Fireblocks workspace and surfaces vault accounts, balances, and transactions as read-only streams.
## Prerequisites
The Fireblocks connector requires Payments module **3.2.0 or higher**. Your stack pins an older version — upgrade to use it.
You need a Fireblocks account with an API key carrying the minimum permissions for the capabilities you use.
## Installation
{"fctl payments connectors install fireblocks config.json"}
With `config.json` containing:
```json
{
"apiKey": "string",
"endpoint": "string",
"name": "string",
"pollingPeriod": "30m",
"privateKey": "string"
}
```
### Configuration fields
| Field | Required | Default | Description |
|---|---|---|---|
| `apiKey` | yes | — | API key from the Fireblocks workspace. |
| `endpoint` | yes | — | Fireblocks API base URL — depends on your workspace environment (see [Endpoints](#endpoints)). |
| `name` | yes | — | Unique name for this connector instance. |
| `pollingPeriod` | no | `30m` | Sync cadence. |
| `privateKey` | yes | — | Fireblocks API secret. Signs the JWT for the `Authorization` header. Single-line string with literal `\n` characters. |
Convert a PEM file to the expected single-line format with:
```bash
cat fireblocks_private.pem | awk '{printf "%s\\n", $0}'
```
### Endpoints
| Environment | URL |
|---|---|
| US Sandbox | `https://sandbox-api.fireblocks.io` |
| US Mainnet/Testnet | `https://api.fireblocks.io` |
| EU Mainnet/Testnet | `https://eu-api.fireblocks.io` |
| EU2 Mainnet/Testnet | `https://eu2-api.fireblocks.io` |
## Capabilities
Read-only — no payouts, transfers, or webhook deliveries.
- **FetchAccounts** — vault accounts.
- **FetchBalances** — per-asset balances per vault.
- **FetchPayments** — transactions, including withdrawals, transfers, staking/unstaking, and minting/burning.
## Account model
Every Payments internal account is one Fireblocks **vault account** (the multi-asset container that holds wallets across chains). The `reference` is the vault `id`; `name` is the vault name; `defaultAsset` is null — vaults are multi-asset, with balances per asset via `FETCH_BALANCES` (same-symbol holdings across chains collapse into one row; see [Multi-chain aggregation](#multi-chain-aggregation)). Vault-specific fields land under [account metadata](#account-metadata). No EXTERNAL accounts are emitted — Fireblocks counterparties live in the workspace UI. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
The canonical asset string is derived from Fireblocks' `displaySymbol`, sanitized where needed, and suffixed with `/precision` (`USDT/6`, `ETH/18`). The Fireblocks `legacyId` (`USDT_ERC20`, `ETH_TEST5`, …) is preserved on every payment under `com.fireblocks.spec/legacy_id`.
### Multi-chain aggregation
Same-symbol holdings across chains in one vault collapse into a single balance row. A vault holding `USDT_ERC20` and `USDT_TRX` produces one `USDT/6` row with the summed amount. Per-chain identity stays observable at the payment level via `com.fireblocks.spec/blockchain_id`, `legacy_id`, and `contract_address`.
### Testnet segregation
Assets on testnet blockchains (per `Blockchain.onchain.test`) carry a `_TEST` suffix and `com.fireblocks.spec/testnet=true` (e.g. `ETH_TEST/18`, `SOL_TEST/9`). Mainnet and testnet holdings of the same symbol never collide.
### Asset classes the connector skips
Only `NATIVE`, `FT`, and `FIAT` are ingested. `NFT`, `SFT`, `VIRTUAL`, and deprecated assets don't appear in Accounts, Balances, or Payments.
### Sanitization rules
When `displaySymbol` can't be used verbatim:
- Lowercase → uppercase (`xDAI` → `XDAI`).
- Digit prefix stripped until first letter (`1INCH` → `INCH`).
- Non-`[A-Z0-9]` characters dropped.
- Base capped at 17 characters; testnet assets additionally carry `_TEST`.
The initial poll pins `after=1`, bypassing Fireblocks' default 90-day window so the full history is backfilled. Long histories may take several cycles to surface entirely, with new rows appearing each cycle.
## Metadata keys
Fireblocks fields land on the `metadata` of the Payments Account or Payment under `com.fireblocks.spec/`. The connector stamps them during mapping so chain, contract, and classification stay visible after the canonical asset string is computed.
### Account metadata
Stamped on a Payments Account when the `VaultAccount` field is set or `true`.
| Key | Source |
|---|---|
| `com.fireblocks.spec/customer_ref_id` | `VaultAccount.customerRefId` |
| `com.fireblocks.spec/hidden_on_ui` | `VaultAccount.hiddenOnUI` |
| `com.fireblocks.spec/auto_fuel` | `VaultAccount.autoFuel` |
### Payment metadata
Two groups of keys land on each Payment:
#### Cached asset metadata
Copied from the connector's in-process Asset cache (loaded at install, refreshed via TTL). Stamped on **every** Payment so consumers retain the chain, contract, and classification even after the asset string is canonicalized away from `legacyId`.
| Key | Source |
|---|---|
| `com.fireblocks.spec/legacy_id` | `Asset.legacyId` |
| `com.fireblocks.spec/asset_uuid` | `Asset.id` |
| `com.fireblocks.spec/display_name` | `Asset.displayName` |
| `com.fireblocks.spec/display_symbol` | `Asset.displaySymbol` |
| `com.fireblocks.spec/blockchain_id` | `Asset.blockchainId` |
| `com.fireblocks.spec/asset_class` | `Asset.assetClass` (`NATIVE`, `FT`, `FIAT`) |
| `com.fireblocks.spec/contract_address` | `Asset.onchain.address` |
| `com.fireblocks.spec/token_standard` | `Asset.onchain.standards` (comma-joined) |
| `com.fireblocks.spec/verified` | `Asset.metadata.verified` (only when `true`) |
| `com.fireblocks.spec/features` | `Asset.metadata.features` (comma-joined) |
| `com.fireblocks.spec/testnet` | derived from `Blockchain.onchain.test` (only when `true`) |
#### Transaction-level metadata
Read directly from the Fireblocks `Transaction`. Present **only when the upstream field is populated**, so absence is meaningful (no `tx_hash` ⇒ off-chain transfer, no `note` ⇒ unannotated, …).
| Key | Source |
|---|---|
| `com.fireblocks.spec/tx_hash` | `Transaction.txHash` |
| `com.fireblocks.spec/network_fee` | `Transaction.feeInfo.networkFee` |
| `com.fireblocks.spec/note` | `Transaction.note` |
| `com.fireblocks.spec/sub_status` | `Transaction.subStatus` |
| `com.fireblocks.spec/destination_ids` | `Transaction.destinations[].id` (multi-destination transactions only) |
The full Fireblocks `Transaction` body is preserved verbatim on `/v3/payments/{paymentID}.adjustments[].raw`. The list endpoint doesn't inline `adjustments` — fetch by ID for the raw payload. Vault payloads are inlined on both the list and detail account endpoints via the account's `raw` field.
## Rate limits
Per endpoint, per transaction, per minute — depending on your Fireblocks contract. See the [Fireblocks rate-limiting docs](https://developers.fireblocks.com/reference/rate-limiting).
## Known gaps
- **Outbound initiation** — Fireblocks supports transaction creation upstream; the connector is read-only. `CreateTransfer` and `CreatePayout` are not wired.
- **Webhooks** — not consumed; refresh runs on the polling cycle.
- **External accounts** — `FETCH_EXTERNAL_ACCOUNTS` is not implemented. Counterparties live in the workspace UI.
- **NFT, SFT, VIRTUAL** — filtered out; only `NATIVE`, `FT`, `FIAT` are ingested (see [Asset model](#asset-model)).
---
## Increase
Source: https://docs.formance.com/modules/payments/connectors/psp/increase
The Increase connector polls an Increase account and surfaces US bank accounts, balances, counterparties, and payments (transfers, ACH, wires, checks). It covers the full US-bank rail set for outbound initiation plus real-time webhook ingest.
The Increase connector requires Payments module **3.1.0 or higher**. Your stack pins an older version — upgrade to use it.
## Prerequisites
You need an Increase account and an API key with read access to accounts, balances, transfers, and inbound credits, plus write access for the outbound rails you initiate.
For webhooks, the connector creates an `EventSubscription` at install signed with the `webhookSharedSecret` you supply; Increase signs every delivery HMAC-SHA256 and the connector verifies before translating.
## Installation
{"fctl payments connectors install increase config.json"}
### Configuration fields
`endpoint` is `https://api.increase.com` for production,
`https://sandbox.increase.com` for sandbox.
## Capabilities
- `FETCH_ACCOUNTS` — internal accounts via `GET /accounts`.
- `FETCH_BALANCES` — available + pending balance per account.
- `FETCH_EXTERNAL_ACCOUNTS` — counterparties via `GET /account_numbers` and `GET /external_accounts`.
- `FETCH_PAYMENTS` — transactions, classified per direction and rail.
- `CREATE_BANK_ACCOUNT` — registers an external account on Increase.
- `CREATE_TRANSFER` — `POST /account_transfers` between two Increase accounts.
- `CREATE_PAYOUT` — ACH (`POST /ach_transfers`), wire (`POST /wire_transfers`), or check (`POST /check_transfers`); rail selected via `com.increase.spec/rail`.
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — `EventSubscription` ingesting `account.*`, `transaction.*`, `*_transfer.*`.
## Account model
Every Payments internal account is one Increase account from `GET /accounts`. The `reference` is the account ID; `name` is the account name; `defaultAsset` is `USD/2` (Increase is USD-only). EXTERNAL accounts come from `/account_numbers` and `/external_accounts`; `v3CreateBankAccount` registers new ones. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
USD-only — every balance and payment is `USD/2`. Amounts are already in minor units.
## Status mapping
| Increase `status` (per rail) | Payment `status` |
| --- | --- |
| `pending_approval`, `pending_submission`, `submitted`, `pending_mailing` (check) | `PENDING` |
| `complete`, `posted`, `mailed` | `SUCCEEDED` |
| `returned`, `rejected`, `failed` | `FAILED` |
| `canceled` | `CANCELLED` |
ACH credits stay `PENDING` until the return window closes — the connector folds it into the `PENDING → SUCCEEDED` transition.
## Metadata keys
Under `com.increase.spec/`:
- **Account**: `account_id`, `account_number`, `routing_number`, `account_type`, `bank` (Increase's underlying program bank).
- **External account**: `account_number_id` or `external_account_id`, `account_number_last4`, `routing_number`, `funding`.
- **Payment**: `transaction_id` or `*_transfer_id`, `rail` (`ach` / `wire` / `check` / `book`), `description`, `idempotency_key`, `network_response_code`.
Outbound initiation requires the rail on the PaymentInitiation metadata:
```json
{
"metadata": {
"com.increase.spec/rail": "ach",
"com.increase.spec/standard_entry_class_code": "PPD"
}
}
```
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (periodic) — per account
└── FetchPayments (periodic) — per account, per rail
FetchExternalAccounts (periodic)
CreateBankAccount / CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
CreateWebhooks (one-shot at install)
```
## Pagination and recovery
Cursor-based (`cursor` + `limit`). The connector persists the latest cursor per stream and resumes on restart; the engine dedupes by `PSPPayment.Reference` since Increase replays events during webhook backfills.
## Known gaps
- **Reverse transfer** is not implemented; ACH returns appear as new payment rows linked via metadata.
- **Real-time payments (RTP / FedNow)** are not exposed — connector covers ACH, wire, and check.
---
## Mangopay
Source: https://docs.formance.com/modules/payments/connectors/psp/mangopay
The Mangopay connector polls a Mangopay client and surfaces multi-currency e-wallets, balances, bank accounts (external), and transactional events. It also initiates transfers and payouts, registers bank accounts, and ingests webhooks.
## Prerequisites
You need a Mangopay client and an API key pair. Mangopay uses HTTP Basic Auth (`clientID:apiKey`); the connector sets the header on every request.
## Installation
{"fctl payments connectors install mangopay config.json"}
### Configuration fields
`endpoint` is `https://api.mangopay.com` for production,
`https://api.sandbox.mangopay.com` for sandbox.
## Capabilities
- `FETCH_ACCOUNTS` — e-wallets, grouped by `UserId`.
- `FETCH_BALANCES` — per-wallet balance via Mangopay's `Balance` resource.
- `FETCH_EXTERNAL_ACCOUNTS` — bank accounts attached to each user.
- `FETCH_PAYMENTS` — payins, transfers, payouts, refunds, and disputes, unified into `PSPPayment`.
- `FETCH_OTHERS` — KYC documents and mandates, surfaced under `mangopay-*` on `v3ListPaymentsOther`.
- `CREATE_BANK_ACCOUNT` — IBAN / US / GB / CA / OTHER variants.
- `CREATE_TRANSFER` — wallet-to-wallet via `POST /transfers`.
- `CREATE_PAYOUT` — wallet-to-bank-account via `POST /payouts/bankwire`.
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — one hook per consumed `EventType`.
## Account model
Every Payments internal account is one Mangopay **e-wallet** (single-currency, scoped to a `UserId`). The `reference` is the wallet ID; `defaultAsset` is the wallet's currency at ISO 4217 precision. Accounts are emitted per user (one fetch cycle per user from the workflow's parent payload). EXTERNAL accounts come from `/users/{UserId}/bankaccounts`; `v3CreateBankAccount` registers new ones (IBAN / US / GB / CA / OTHER). See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Multi-currency (EUR / USD / GBP, …), formatted to UMN at ISO 4217 precision. Amounts are already in minor units — no scaling.
## Status mapping
| Mangopay `Status` (Transactions) | Payment `status` |
| --- | --- |
| `CREATED`, `IN_PROGRESS` | `PENDING` |
| `SUCCEEDED` | `SUCCEEDED` |
| `FAILED` | `FAILED` |
| `CANCELLED` | `CANCELLED` |
`CREATE_*` initiations schedule `PollTransferStatus` / `PollPayoutStatus` against the relevant endpoint until terminal.
## Metadata keys
Under `com.mangopay.spec/`:
- **Account**: `wallet_id`, `description`, `owners` (comma-joined `UserId` list), `currency`.
- **External account**: `bank_account_id`, `type` (`IBAN` / `US` / `GB` / `CA` / `OTHER`), `iban`, `bic`, `owner_name`, `owner_address`.
- **Payment**: `transaction_id`, `transaction_type` (`PAYIN` / `PAYOUT` / `TRANSFER` / `REFUND`), `nature` (`REGULAR` / `REPUDIATION` / `REFUND` / `SETTLEMENT`), `result_code`, `result_message`.
Bank-account creation uses `com.mangopay.spec/bank_account_type` on the `v3CreateBankAccount` body to select the variant (default `IBAN`).
## Workflow tree
```text
FetchAccounts (periodic) — paginated over UserList → WalletList
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per wallet
FetchExternalAccounts (periodic) — per user
FetchOthers (periodic) — KYC docs, mandates
CreateBankAccount / CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
CreateWebhooks (one-shot at install) — one hook per EventType
```
## Pagination and recovery
1-indexed `Page` + `Per_Page` (max 100). The connector persists the watermark per stream in platform-managed `State`. Webhook deliveries dedupe by `(EventType, ResourceId)` against `PSPPayment.Reference`.
## Known gaps
- **Pre-authorizations**: not surfaced; only successful captures land as `PSPPayment`s.
- **Disputes**: arrive via webhooks as `PSPPayment` rows tagged `transaction_type=DISPUTE`; the full lifecycle lives in Mangopay's dashboard.
- **KYC**: documents surface via `FETCH_OTHERS` only — submission happens in Mangopay's UI.
---
## Modulr
Source: https://docs.formance.com/modules/payments/connectors/psp/modulr
The Modulr connector polls a Modulr customer and surfaces UK/EU bank accounts, balances, beneficiaries, and transactions. It also initiates transfers between Modulr accounts and payouts to registered beneficiaries.
## Prerequisites
You need a Modulr customer and an API key + secret pair. Modulr signs every request with HMAC-SHA1; the connector builds the `Authorization` header automatically.
## Installation
{"fctl payments connectors install modulr config.json"}
### Configuration fields
`endpoint` is `https://api-sandbox.modulrfinance.com/api-sandbox` for sandbox, `https://api.modulrfinance.com/api-live` for production.
## Capabilities
- `FETCH_ACCOUNTS` — bank accounts via `GET /accounts`.
- `FETCH_BALANCES` — available + reserved per account.
- `FETCH_EXTERNAL_ACCOUNTS` — beneficiaries via `GET /beneficiaries`.
- `FETCH_PAYMENTS` — transactions, classified PAY-IN / PAYOUT / TRANSFER.
- `CREATE_TRANSFER` — `POST /payments` between two Modulr accounts on the same rail.
- `CREATE_PAYOUT` — `POST /payments` to a beneficiary (Faster Payments for GBP, SEPA for EUR).
`CREATE_BANK_ACCOUNT` is not implemented. Webhooks are not wired.
## Account model
Every Payments internal account is one Modulr account from `GET /accounts`. The `reference` is the account ID; `name` is the account name; `defaultAsset` is the account's `currency` (Modulr supports GBP and EUR). EXTERNAL accounts come from `/beneficiaries`. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
GBP and EUR at the account level — `GBP/2` / `EUR/2`. Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
| Modulr `status` | Payment `status` |
| --- | --- |
| `SUBMITTED`, `VALIDATED`, `SCHEDULED`, `PROCESSING` | `PENDING` |
| `PROCESSED`, `CONFIRMED` | `SUCCEEDED` |
| `ER_INVALID`, `ER_EXTSYS`, `ER_GENERAL` | `FAILED` |
| `CANCELLED`, `RECALLED` | `CANCELLED` |
`CREATE_TRANSFER` and `CREATE_PAYOUT` schedule `PollTransferStatus` / `PollPayoutStatus` against `/payments/{id}` until terminal.
## Metadata keys
Under `com.modulr.spec/`:
- **Account**: `account_id`, `account_number`, `sort_code` (UK), `iban` (EU), `bic`, `customer_id`.
- **External account**: `beneficiary_id`, `account_number`, `sort_code`, `iban`, `bic`, `name`.
- **Payment**: `transaction_id`, `payment_id`, `reference`, `end_to_end_id`, `payment_method` (`PAYOUT` / `SEPA_CT` / `FPS_OUT` / …), `direction`.
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per account
FetchExternalAccounts (periodic)
CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
```
## Pagination and recovery
0-indexed `page` + `size` (max 100). Watermarks persist in platform-managed `State`; restarts resume from the last committed page boundary.
## Known gaps
- **Webhooks** are not implemented — subscription provisioning is not wired.
- **CHAPS** (high-value GBP rail) is not exposed; outbound covers Faster Payments and SEPA only.
- **Beneficiary creation** through the Payments module is not wired; manage beneficiaries in the Modulr portal.
---
## Moneycorp
Source: https://docs.formance.com/modules/payments/connectors/psp/moneycorp
The Moneycorp connector polls a Moneycorp account and surfaces multi-currency wallets, balances, beneficiaries, and transactions. It also initiates transfers between Moneycorp accounts and payouts to registered beneficiaries.
## Prerequisites
You need a Moneycorp account and a `clientID` + `apiKey` pair. Moneycorp uses OAuth2 client-credentials; the connector exchanges the credentials for a short-lived bearer token and refreshes automatically.
## Installation
{"fctl payments connectors install moneycorp config.json"}
### Configuration fields
`endpoint` defaults to `https://api.moneycorp.com`. Sandbox: `https://sandbox-corpapi.moneycorp.com`.
## Capabilities
- `FETCH_ACCOUNTS` — wallets per ledger.
- `FETCH_BALANCES` — per-account balance.
- `FETCH_EXTERNAL_ACCOUNTS` — beneficiaries.
- `FETCH_PAYMENTS` — transactions and FX conversions.
- `CREATE_TRANSFER` — wallet-to-wallet.
- `CREATE_PAYOUT` — payment to a registered beneficiary.
`CREATE_BANK_ACCOUNT` is not implemented; manage beneficiaries through Moneycorp. Webhooks are not wired.
## Account model
Every Payments internal account is one Moneycorp wallet under the configured client/ledger. The `reference` is the Moneycorp account ID (numeric, used as the cursor since Moneycorp doesn't expose creation dates); `name` is the account name; `defaultAsset` is null — wallets are multi-currency, with one balance row per asset via `FETCH_BALANCES`. EXTERNAL accounts come from Moneycorp's beneficiaries endpoint. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Multi-currency, formatted to UMN at ISO 4217 precision. Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
| Moneycorp `status` | Payment `status` |
| --- | --- |
| `pending`, `processing` | `PENDING` |
| `completed`, `released` | `SUCCEEDED` |
| `failed`, `cancelled-due-to-failure` | `FAILED` |
| `cancelled` | `CANCELLED` |
`CREATE_TRANSFER` and `CREATE_PAYOUT` schedule `PollTransferStatus` / `PollPayoutStatus` until terminal.
## Metadata keys
Under `com.moneycorp.spec/`:
- **Account**: `account_id`, `account_name`, `account_type`, `client_reference`.
- **External account**: `beneficiary_id`, `country`, `currency`, `bank_account_number`, `iban`, `bic`, `routing_code_type1`.
- **Payment**: `transaction_id`, `payment_id`, `payment_type`, `currency`, `client_reference`, `reason`.
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (periodic) — per account
└── FetchPayments (periodic) — per account
FetchExternalAccounts (periodic)
CreateTransfer / CreatePayout (event-driven)
└── PollTransferStatus / PollPayoutStatus until terminal
```
## Pagination and recovery
1-indexed `pageNumber` + `pageSize`. Watermarks persist in platform-managed `State`; restarts resume from the last committed page boundary.
## Known gaps
- **Webhooks** are not implemented.
- **FX conversions** surface as `PSPPayment`s with `com.moneycorp.spec/payment_type=fx`, not as separate [Conversion](/modules/payments/conversions) entries.
- **Beneficiary creation** through the Payments module is not wired.
---
## Bitstamp
Source: https://docs.formance.com/modules/payments/connectors/exchange/bitstamp
The Bitstamp connector polls a Bitstamp account and surfaces currency wallets, balances, payments, trading orders, and conversions. It is read-only and spot-only.
Bitstamp API keys scope to a single account — Main or one named sub-account — so **install one connector instance per Bitstamp account** you need to reconcile.
## Prerequisites
You need a Bitstamp account and an API key with the minimum permissions for the capabilities you use. Bitstamp uses HMAC-SHA256 v2 signing; the connector signs internally — you only supply the key and secret.
## Installation
{"fctl payments connectors install bitstamp config.json"}
With `config.json` containing:
```json
{
"apiKey": "string",
"apiSecret": "string",
"endpoint": "https://www.bitstamp.net",
"name": "string",
"pollingPeriod": "30m"
}
```
### Configuration fields
| Field | Required | Default | Description |
|---|---|---|---|
| `apiKey` | yes | — | Bitstamp API key. Sent in `X-Auth` as `BITSTAMP `. |
| `apiSecret` | yes | — | HMAC-SHA256 signing secret. Never logged. |
| `endpoint` | no | `https://www.bitstamp.net` | API root. Override only for non-production. |
| `name` | yes | — | Unique name for this connector instance (e.g. `bitstamp-main`, `bitstamp-treasury` when running one per scope). |
| `pollingPeriod` | no | `30m` | Sync cadence (min `20m`). Drives every capability. |
The config is deliberately minimal — the API key scopes the connection, and Bitstamp's API exposes no portable way to fan out across scopes.
## Capabilities
- **FetchAccounts** — currency wallets in scope via `POST /api/v2/account_balances/`.
- **FetchBalances** — re-reads `account_balances/` per cycle.
- **FetchPayments** — `user_transactions/` on a single `since_id` watermark.
- **FetchOrders** — open-orders snapshot reconciled against `order_status/` per tracked id.
- **FetchConversions** — `user_transactions/` rows with `type=36` (instant buy/sell).
Payouts, transfers, webhooks, and bank-account creation are not implemented; Bitstamp's API surface for those flows is uneven.
## Account model
Every Payments internal account is one currency in the Bitstamp scope — one account per `(connector install, currency)`. The `reference` is the currency ticker (`USD`, `EUR`, `BTC`); the connector-level `name` (e.g. `bitstamp-main`) disambiguates the scope. `defaultAsset` is `TICKER/precision` from the `currencies` cache. No EXTERNAL accounts are emitted. See [Accounts](/modules/payments/accounts) for the cross-connector model.
Bitstamp returns every currency the account *could* hold. Rows with `Available`, `Total`, and `Reserved` all zero are skipped — emitting hundreds of empty accounts pollutes the catalogue without informing anyone.
Bitstamp doesn't expose per-currency creation dates, so `CreatedAt` defaults to `BitstampGenesis = 2011-08-02 UTC` (the platform's launch). The sentinel is stable across reinstalls.
## Asset model
The canonical asset is the uppercased currency ticker with precision suffix from the `currencies` cache — `USD/2`, `EUR/2`, `BTC/8`, `USDT/6`. The cache loads at install and refreshes on a TTL; assets not in the cache are logged and skipped rather than emitted with a guessed precision.
## Workflow tree
```text
FetchAccounts (periodic)
└── FetchOrders (periodic, derives tracked markets from accounts)
FetchBalances (periodic root)
FetchPayments (periodic root)
FetchConversions (periodic root)
```
`FetchOrders` nests under `FetchAccounts` because it derives tradeable markets from account metadata. Balances, Payments, and Conversions are independent roots — their Bitstamp endpoints are account-global at the API-key level, so no parent context is needed.
## Payments
A Payment is one row from `user_transactions/`, polled on a single inclusive `since_id` watermark. Trade rows (`type=2`) feed [Orders](#orders); instant-buy/sell rows (`type=36`) feed [Conversions](#conversions); everything else (deposits, withdrawals, settled activity, sub-account transfer legs of types 14 / 33 / 35) lands as a Payment.
The watermark is inclusive — the last row of cycle N reappears as the first of N+1, deduped downstream by `PSPPayment.Reference`. End-of-pagination keeps the watermark; we never reset.
Sub-account transfer rows (types 14 / 33 / 35) are mapped defensively — signed PAY-IN / PAYOUT legs sharing a `transfer_pair_id`. A Main-account API key does not actually surface them on `user_transactions/`. Customers needing transfer reconciliation install one connector per sub-account; the pair-id correlation works once both legs' keys are integrated.
## Orders
Bitstamp doesn't expose an "orders since X" endpoint. The connector reconciles a live snapshot every cycle:
1. `GetOpenOrders` returns currently-open orders.
2. New IDs are seeded into `trackedOrders` state with their first-sight `LimitPrice`.
3. `GetOrderStatus` is called per id (snapshot ∪ tracked) for fills, fees, datetime, and market.
4. The order maps to a `PSPOrder` with adjustments aggregating each observed state change.
5. Tracked entries drop on terminal status (`FILLED` / `CANCELLED`).
6. Tracked entries also drop after `FirstSeenAt + 25d`, emitting `com.bitstamp.spec/retention_expired = true`. Bitstamp retains `order_status/` rows for 30 days; the 5-day margin avoids losing the terminal state.
`Trade` primitives in `user_transactions/` (`type=2` rows with a parent `order_id`) aggregate under their parent rather than being emitted as standalone Orders — one `PSPOrder` per Bitstamp order, fills attached.
## Conversions
`user_transactions/` returns two primitives that both look like "buys" and "sells" in the web UI:
| Wire | Has `order_id`? | Lifecycle | Formance model |
|---|---|---|---|
| `type=2` (Trade — order fill) | yes | order-book — In Queue → Open → Finished / Cancelled | `PSPOrder` |
| `type=36` (Instant buy/sell) | no | atomic — settled in one round-trip | `PSPConversion` |
Conversions share the `user_transactions/` stream with payments but hold their own watermark — the two cursors advance independently. Asset class plays no role in classification: Bitstamp tags every crypto (BTC, USDC, EURC, …) as `currency.type = "crypto"` with no stablecoin tag. A `type=36` BTC↔EUR row and a `type=36` USDC↔EUR row are the same primitive; consumers wanting "market exposure" vs "stable-value swap" semantics apply their own allow-list against `SourceAsset` / `DestinationAsset`.
## Install-time enrichment
Four reference datasets load in parallel at install, refreshed via TTL cache:
- `markets` — every trading pair, used to resolve order quote/base currencies.
- `my_markets` — pairs the key has actually traded (gates Order details).
- `fees/trading` — per-market trading fees, surfaced on Order metadata.
- `fees/withdrawal` — per-currency withdrawal fees, surfaced on withdrawal-request payments.
Permission-gated endpoints feed a process-lifetime `derivSkip` cache: the first `403`-style response for a key without `my_markets` scope logs once at Info, then subsequent attempts go silent. Keeps logs readable on read-only keys without trading scope.
## Metadata keys
Under `com.bitstamp.spec/`. Full list in the connector's [`MAPPINGS.md`](https://github.com/formancehq/payments/blob/main/ee/plugins/bitstamp/MAPPINGS.md); highlights:
- **Account**: `currency_type`, `currency_decimals`, `withdrawal_fee?`, `is_crypto?`.
- **Payment**: `tx_type`, `bank_transaction_id?`, `transfer_pair_id?`, `transfer_direction?` (set on the defensive sub-account transfer legs).
- **Order**: `order_subtype` (`LIMIT` / `MARKET` / `INSTANT` / `STOP_LIMIT`), `order_status_datetime`, `client_order_id?`, `historical?`, `retention_expired?`.
- **Conversion**: `from_amount_raw`, `to_amount_raw`, `fee_market`.
## Pagination and recovery
`FetchPayments` and `FetchConversions` each persist a `LastTransactionID` watermark and advance only after the cycle completes — a mid-cycle worker crash replays the same page on restart, with downstream dedupe absorbing the overlap. `FetchOrders` checkpoints `LastSeenEventIDPerMarket` plus `HasMoreCurrentMarket` so a partial paginated walk resumes from the same market on the next cycle.
## Known gaps
- **Historical orders** — orders placed and filled before install, or older than 30 days, fall outside Bitstamp's `order_status/` retention and aren't back-filled. Per-fill rows exist in `user_transactions/` as `type=2` with `order_id`; `MAPPINGS.md §9` documents the aggregation approach.
- **No programmatic sub-account discovery** — Bitstamp's API doesn't expose a "list my scopes" call. Deploy one connector per account scope, named via `name`.
---
## Qonto
Source: https://docs.formance.com/modules/payments/connectors/psp/qonto
The Qonto connector polls a Qonto organization and surfaces business bank accounts, balances, beneficiaries, and transactions. It is **read-only**: Qonto's outbound transfer and webhook surfaces require three-legged OAuth2, which the framework does not drive.
## Prerequisites
You need a Qonto organization and an API key pair. Qonto authenticates with `login:secret-key` in the `Authorization` header; staging additionally requires a `stagingToken` header.
## Installation
{"fctl payments connectors install qonto config.json"}
### Configuration fields
Production: `https://thirdparty.qonto.com`. Staging: `https://thirdparty.staging.qonto.co` with a `stagingToken`.
## Capabilities
- `FETCH_ACCOUNTS` — bank accounts via `GET /v2/organizations`.
- `FETCH_BALANCES` — authorized + balance per account.
- `FETCH_EXTERNAL_ACCOUNTS` — beneficiaries.
- `FETCH_PAYMENTS` — `GET /v2/transactions`, classified per direction.
Qonto exposes outbound transfer initiation and webhooks upstream, but both require three-legged OAuth2 that the framework doesn't yet drive — the connector advertises only the read capabilities.
## Account model
Every Payments internal account is one Qonto bank account under the organization. `GET /v2/organizations` returns the whole tree at once — there's no per-account endpoint. The `reference` is the bank-account ID; `name` is the account name; `defaultAsset` is `EUR/2` (Qonto is EUR-only). EXTERNAL accounts come from Qonto beneficiaries. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
EUR-only — every balance and payment is `EUR/2`. Amounts are already in minor units.
## Status mapping
| Qonto transaction `status` | Payment `status` |
| --- | --- |
| `pending` | `PENDING` |
| `completed` | `SUCCEEDED` |
| `declined` | `FAILED` |
| `reversed` | `CANCELLED` |
| anything else | `UNKNOWN` |
## Metadata keys
Under `com.qonto.spec/`:
- **Account**: `iban`, `bic`, `currency`, `organization_slug`.
- **External account**: `beneficiary_id`, `iban`, `bic`, `bank_name`, `trusted` (boolean).
- **Payment**: `transaction_id`, `operation_type` (`card` / `transfer` / `direct_debit` / `cheque` / `swift_income` / …), `side` (`credit` / `debit`), `reference`, `note`.
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per account
FetchExternalAccounts (periodic)
```
## Pagination and recovery
1-indexed `current_page` + `per_page` (max 100). The connector persists the watermark per stream in platform-managed `State`.
## Known gaps
- **Outbound initiation** is not implemented — Qonto's `CreateTransfer` API requires 3-legged OAuth2.
- **Webhooks** are not implemented for the same reason.
- **Multi-organization tenants**: one connector install covers one organization.
---
## Stripe
Source: https://docs.formance.com/modules/payments/connectors/psp/stripe
The Stripe connector polls a Stripe account and surfaces `acct_*` balances, charges, transfers, payouts, payments, and refunds. It initiates transfers and payouts via v3 PaymentInitiation, and ingests Stripe webhooks for real-time updates.
## Prerequisites
You need a Stripe account and a restricted API key scoped to charges, transfers, payouts, balance, balance transactions, and external accounts. Webhook creation additionally requires `webhook_endpoints:write`.
## Installation
{"fctl payments connectors install stripe config.json"}
### Configuration fields
The connector uses the official `stripe-go/v80` SDK with the `apiKey` as a Bearer token. The webhook secret is not in the config — Stripe assigns one when the connector creates the endpoint at install, and the platform stores it out-of-band.
## Capabilities
- `FETCH_ACCOUNTS` — connected accounts plus the platform's own `acct_*`, paginated via `starting_after`.
- `FETCH_BALANCES` — available + pending balance per currency, via `GET /v1/balance` per connected account.
- `FETCH_EXTERNAL_ACCOUNTS` — external bank accounts and debit cards per connected account.
- `FETCH_PAYMENTS` — charges, refunds, transfers, payouts, and balance transactions, unified into `PSPPayment`.
- `CREATE_TRANSFER` — `POST /v1/transfers` between platform-controlled accounts.
- `CREATE_PAYOUT` — `POST /v1/payouts` to an attached external account.
- `CREATE_WEBHOOKS` — provisions a `WebhookEndpoint` at install for the events the connector consumes.
- `TRANSLATE_WEBHOOKS` — converts `charge.*`, `payout.*`, `transfer.*`, `balance.available`, etc. to Payments events.
`CREATE_BANK_ACCOUNT` is not implemented — Stripe expects account holders to attach externals via the dashboard or Stripe-issued links.
## Account model
Every Payments internal account is one Stripe `acct_*` (a connected account or the platform's own root). The `reference` is the Stripe account ID, except for the platform's own which uses the legacy literal `root` for backwards compatibility; `name` is the display name; `defaultAsset` is the `default_currency` for single-currency accounts, null on multi-currency. EXTERNAL accounts come from each connected account's external bank accounts and debit cards. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Stripe returns lowercase ISO 4217 (`usd`, `eur`, `jpy`), formatted to UMN at standard precision (`USD/2`, `EUR/2`, `JPY/0`). Amounts are already in minor units — no scaling.
## Status mapping
| Stripe `status` / event family | Payment `status` |
| --- | --- |
| `pending`, `in_transit`, `paid` (payout pre-settlement) | `PENDING` |
| `succeeded`, `paid` (charge), `completed` | `SUCCEEDED` |
| `failed`, `requires_payment_method` (terminal) | `FAILED` |
| `canceled` | `CANCELLED` |
| anything else | `UNKNOWN` |
Refunds land as separate `PAY-IN` rows against the original payment's account, linked via `com.stripe.spec/refund_of`.
## Metadata keys
Under `com.stripe.spec/`:
- **Account**: `type` (`standard`, `express`, `custom`), `country`, `default_currency`, `details_submitted`, `payouts_enabled`, `charges_enabled`.
- **Payment**: `payment_method`, `payment_method_type` (`card`, `sepa_debit`, `us_bank_account`, …), `application_fee_amount`, `transfer_group`, `refund_of` (when applicable).
- **External account**: `routing_number`, `last4`, `bank_name`, `fingerprint`.
## Workflow tree
```text
FetchAccounts (periodic)
├── FetchBalances (FromPayload — no extra API call beyond /v1/balance per account)
├── FetchPayments (periodic) — Charges → Refunds → Transfers → Payouts → Balance Transactions
└── FetchExternalAccounts (periodic) — per connected account
CreateWebhooks (one-shot at install)
```
## Pagination and recovery
Cursor-based via `starting_after` per resource. The connector persists the latest cursor per stream in platform-managed `State`; restarts resume from the last committed cursor (Stripe's at-least-once event semantics aside). The engine dedupes by `PSPPayment.Reference`.
## Known gaps
- **Bank-account creation** is not implemented; account holders attach externals via the Stripe dashboard or Financial Connections.
- **Reverse transfer / reverse payout** is not wired; refunds appear as separate `PAY-IN` rows linked via metadata.
- **Multi-currency accounts**: balance per `(account, currency)`. Stripe doesn't expose a "default asset" on multi-currency accounts, so `defaultAsset` is null in that case.
---
## Kraken Pro
Source: https://docs.formance.com/modules/payments/connectors/exchange/krakenpro
The Kraken Pro connector polls a Kraken Pro account and surfaces its asset wallets, balances, payments, trading orders, and conversions. It is read-only and spot-only.
Kraken Pro API keys scope to a single account, so **install one connector instance per Kraken Pro account** you need to get data from.
## Prerequisites
You need a Kraken Pro account and an API key. Kraken signs requests with HMAC-SHA512; the connector signs internally — you supply only the key and secret.
The API key must carry at least the following scopes:
- **Funds** → Query
- **Orders & trades** → Query closed orders & trades
- **Data** → Query ledger entries
## Installation
{"fctl payments connectors install krakenpro config.json"}
With `config.json` containing:
```json
{
"apiKey": "string",
"apiSecret": "string",
"endpoint": "https://api.kraken.com",
"name": "string",
"pollingPeriod": "30m"
}
```
### Configuration fields
| Field | Required | Default | Description |
|---|---|---|---|
| `apiKey` | yes | — | Kraken Pro API key. |
| `apiSecret` | yes | — | Kraken Pro private key. |
| `endpoint` | yes | — | Kraken Pro API base URL, e.g. `https://api.kraken.com`. |
| `name` | yes | — | Unique name for this connector instance (e.g. `krakenpro-main`, `krakenpro-treasury` when running one per account). |
| `pollingPeriod` | no | `30m` | Sync cadence (min `20m`). Drives every capability. |
The config is deliberately minimal — the API key scopes the connection to a single Kraken Pro account, and Kraken's API exposes no portable way to fan out across accounts.
## Capabilities
The Kraken Pro connector supports the following read-only capabilities:
- **FetchAccounts** — one account per asset variant present via `POST /0/private/BalanceEx`.
- **FetchBalances** — derived from the same `BalanceEx` call, no extra hop.
- **FetchPayments** — deposits, withdrawals, transfers, staking, rewards, and adjustments from `POST /0/private/Ledgers`.
- **FetchOrders** — closed/historical orders from `POST /0/private/ClosedOrders`.
- **FetchConversions** — off-orderbook swaps from `POST /0/private/Ledgers`, grouped by `refid`.
## Account model
Each Payments account maps to one Kraken asset wallet. Kraken keeps a separate wallet per asset variant — the spot balance, staked balance, rewards balance, and so on are distinct — and each becomes its own account. The `reference` is Kraken's asset code (`XXBT`, `XBT.M`, `ZUSD`, `ADA.S`), a `wallet_type` metadata key records the variant (`spot`, `staked`, `rewards`, `yield`, `earn`, `parachain`, `tokenised`, `hold`, `margin`), and `defaultAsset` is the normalized `TICKER/precision` (see [Asset model](#asset-model)). Only assets the account holds or has held are surfaced. See the generic [Accounts](/modules/payments/accounts) page for the cross-connector model.
Kraken doesn't expose a creation date per asset wallet, so `CreatedAt` is set to a fixed placeholder — Kraken's launch date, `2011-08-01T00:00:00Z`.
## Asset model
Kraken uses its own asset codes (`XXBT`, `ZUSD`, `XETH`). The connector normalizes them to standard tickers — `XXBT` → `BTC`, `ZUSD` → `USD`, `XETH` → `ETH` — so assets appear under the symbols you expect.
Precision follows Kraken's internal precision for each asset, which is finer than common market conventions (e.g. `BTC/10` rather than `BTC/8`, `USD/4` rather than `USD/2`). This keeps every amount exact — no value is rounded or truncated on the way in.
## Status mapping
Payments come from Kraken's Ledgers endpoint, which only writes an entry on settlement — there is no pending state at this layer, so every Payment is `SUCCEEDED`.
Order status is derived from Kraken's `status` enum combined with the filled-vs-ordered volume:
| Kraken `status` | `vol_exec` vs `vol` | Order `status` |
|---|---|---|
| `closed` | exec ≥ vol | `FILLED` |
| `closed` | 0 < exec < vol | `PARTIALLY_FILLED` |
| `closed` | 0 | `CANCELLED` |
| `canceled` | exec > 0 | `PARTIALLY_FILLED` |
| `canceled` | 0 | `CANCELLED` |
| `expired` | — | `EXPIRED` |
## Payments
A Payment is one row from `/0/private/Ledgers`. Kraken's ledger `type` enum maps as follows:
| Kraken `type` | Payment `type` |
|---|---|
| `deposit` | `PAYIN` |
| `withdrawal` | `PAYOUT` |
| `transfer`, `custodytransfer` | `TRANSFER` |
| `staking`, `reward`, `dividend`, `credit`, `nft_rebate` | `PAYIN` |
| `nftcreatorfee` | `PAYOUT` |
| `adjustment`, `rollover`, `settled`, `reserve`, `ic_settlement`, … | `OTHER` |
| `trade`, `eqtrade` | skipped — handled by [Orders](#orders) |
| `conversion`, `sale`, `marginconversion`, `margin_conversion` | skipped — handled by [Conversions](#conversions) |
The Payment `reference` is the ledger entry id (not `refid`, which groups multi-leg events). The row's `fee` is recorded in metadata but not subtracted from the amount. Unknown future `type` values fall back to `OTHER` with a warning log.
## Orders
Only closed orders are registered — an order is surfaced once it has been filled, cancelled, or expired. In-flight orders that are still open or partially filled are not tracked while they remain active.
## Conversions
Conversions share the `/0/private/Ledgers` stream with payments but classify a distinct type set — `conversion`, `sale`, `marginconversion`, `margin_conversion` (plus derivatives variants for exhaustiveness; spot-only accounts see only the first two). A conversion is a **pair** of ledger rows sharing one `refid`: a negative-amount leg (source asset) and a positive-amount leg (destination asset).
`SourceAmount` / `DestinationAmount` are gross; `fee` is the sum across both legs.
## Metadata keys
All keys are namespaced `com.krakenpro.spec/`:
- **Account**: `wallet_type` (`spot` / `staked` / `rewards` / …).
- **Payment / Conversion**: `refid`, `kraken_type`, `subtype`, `aclass`, `balance_after`; `fee` (payments); `source_ledger_id`, `destination_ledger_id` (conversions).
- **Order**: `pair`, `ws_name`, `ordertype`, `price_asset`, `fills` (comma-separated fill txids), `cl_ord_id?`.
## Known gaps
- Orders appear only once they're closed (filled, cancelled, or expired) — orders that are still open or partially filled aren't shown while active.
- A Kraken Pro API key is scoped to a single account, so reconciling multiple accounts means installing one connector per account.
---
## Wise
Source: https://docs.formance.com/modules/payments/connectors/psp/wise
The Wise connector polls a Wise Business profile and surfaces multi-currency balances, recipients, and transactions. It initiates transfers between your balances and payouts to recipients across the Wise rail network, and ingests Wise webhooks.
## Prerequisites
You need a Wise Business profile and an API token with read access to balances, recipients, and transfers, plus write access for the outbound rails you initiate. The connector creates a webhook subscription at install and verifies deliveries against the `webhookPublicKey` you supply — mis-signed events are rejected.
## Installation
{"fctl payments connectors install wise config.json"}
### Configuration fields
## Capabilities
- `FETCH_ACCOUNTS` — multi-currency balances per profile via `GET /v4/profiles/{profileId}/balances`.
- `FETCH_BALANCES` — derived from the Accounts payload.
- `FETCH_EXTERNAL_ACCOUNTS` — recipients via `GET /v1/accounts`.
- `FETCH_PAYMENTS` — transfers with full lifecycle.
- `FETCH_OTHERS` — quotes and requirements, surfaced under `wise-*` on `v3ListPaymentsOther`.
- `CREATE_TRANSFER` — `POST /v1/transfers` between your own balances (after a quote).
- `CREATE_PAYOUT` — `POST /v1/transfers` to a registered recipient (after a quote).
- `CREATE_WEBHOOKS` + `TRANSLATE_WEBHOOKS` — subscribes to `transfers#state-change` and related events.
`CREATE_BANK_ACCOUNT` is not implemented; manage recipients via the Wise dashboard or `POST /v1/accounts`.
## Account model
Every Payments internal account is one Wise **balance** (single-currency, scoped to a profile). The `reference` is the balance ID; `name` is the balance name; `defaultAsset` is the balance currency at ISO 4217 precision. Accounts are emitted per profile. EXTERNAL accounts come from `/v1/accounts` (Wise recipients). See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Multi-currency, formatted to UMN at ISO 4217 precision (`USD/2`, `EUR/2`, `JPY/0`, …). Amounts arrive as decimal strings; the connector applies major-to-minor scaling.
## Status mapping
| Wise transfer `status` | Payment `status` |
| --- | --- |
| `incoming_payment_waiting`, `processing`, `funds_converted`, `outgoing_payment_sent` | `PENDING` |
| `outgoing_payment_received` (terminal for the rail), `funds_refunded` (for refund leg) | `SUCCEEDED` |
| `bounced_back`, `charged_back`, `failed` | `FAILED` |
| `cancelled` | `CANCELLED` |
`outgoing_payment_sent` is non-terminal — the connector keeps the transfer in `PENDING` until destination-side receipt is confirmed.
## Metadata keys
Under `com.wise.spec/`:
- **Account**: `balance_id`, `profile_id`, `type` (`STANDARD` / `SAVINGS`), `currency`.
- **External account / recipient**: `account_holder_name`, `currency`, `country`, `bank_name`, `iban`, `swift_code`, `legal_entity_type`.
- **Payment**: `transfer_id`, `source_currency`, `target_currency`, `quote_id`, `source_amount`, `target_amount`, `rate`, `fee`, `reference`.
## Workflow tree
```text
FetchAccounts (periodic) — per profile
├── FetchBalances (FromPayload — no extra API call)
└── FetchPayments (periodic) — per profile
FetchExternalAccounts (periodic) — per profile
FetchOthers (periodic) — quotes, requirements
CreateTransfer / CreatePayout (event-driven)
├── Create quote (POST /v3/quotes)
├── Create transfer (POST /v1/transfers)
├── Fund transfer (POST /v3/transfers/{id}/payments)
└── PollTransferStatus / PollPayoutStatus until terminal
CreateWebhooks (one-shot at install)
```
## Pagination and recovery
`offset` + `limit` (max 100). Watermarks persist per profile / per stream in platform-managed `State`. Webhook deliveries dedupe at the engine level by `PSPPayment.Reference`.
## Known gaps
- **Quote regeneration** is handled inside `CreateTransfer`, but if the rate moves enough between quote and funding the transfer can fail with `quote_expired` — the PaymentInitiation lands in `FAILED`; retry to issue a fresh quote.
- **Profile multi-tenant**: one connector install covers every profile attached to the token (personal + business). Filter on `metadata.profile_id` to slice.
---
## Wallets
Source: https://docs.formance.com/modules/wallets
Wallets is a fully managed, white-label wallet service to materialize and spend users' funds. It comes with built-in support for multi-currency balances and temporary holds capabilities (and upcoming support for reserved funds and expirable fungibles).
It is built on top of the Formance Ledger service and is designed to provide an easy way to add wallet capabilities to your application without having to worry about the underlying transaction structure, providing an opinionated model implementation.
## Use-cases
Wallets can be a great fit if you are building:
* A closed-loop economy on top of your marketplace, where funds paid out can be used at checkout
* Savings on processing fees, by keeping some transactions internal to your system
* A better repeat purchase rate, by implementing flows like refund to wallet
## Using Wallets vs Ledger
An honest question that might come to mind is: what's the difference between using Wallets and using Ledger directly? The answer is that the Ledger is a low-level service that provides a generic way to store and retrieve transactions, while Wallets is a higher-level service that provides out-of-the-box capabilities that would require otherwise non-trivial effort to implement on top of the Ledger.
Note that you can start using Wallets now, and eject at any time to use the Ledger directly if you start outgrowing it, at the cost of having to implement some of the Wallets features yourself.
---
## Prerequisites
Source: https://docs.formance.com/modules/wallets/prerequisites
For this tutorial, you will need to have a Formance Cloud Sandbox provisioned.
Formance Cloud Sandbox is a free, fully functional, trial environment for Formance Enterprise Edition that you can use to learn about Formance Stack and to develop and test your applications.
You can create a Formance Cloud Sandbox by following the instructions in the [Getting Started guide](/getting-started/quickstart).
---
## Basic wallet operations
Source: https://docs.formance.com/modules/wallets/basic-operations
In this section, you'll use Wallets to model basic RPG game mechanics.
You'll manage the wallet of Eryldor, an elf mage living through countless adventures!
## An introduction to Wallets
At its core, a **wallet** is a container of assets related to one entity. It can be used to store and manage any kind of assets, from money to items, and even more complex objects.
In the context of Cones of Dunshire, a wallet is used to store the money, currencies, and points of a character.
A wallet is composed of **balances**. A balance is a mechanism to logically separate assets according to nature, usage, lifecycle or any other criteria. A balance can be seen as a sub-wallet, and can be used to store and manage a specific set of assets. A balance can expire. When a balance expires, its assets cannot be used anymore.
In Cones of Dunshire, Eryldor has a wallet with two balances: one for coins and experience points, and another for badges, which are earned in tournaments and expire when the tournament season ends.
Here is the structure of Eryldor's wallet.

## Creating Eryldor's wallet
Before starting, make sure you have followed the [prerequisites](/getting-started/quickstart) and that you're logged in with the `fctl login` command.
First you'll create Eryldor's wallet. You'll use the `fctl wallet create` command to create a wallet named `eryldor`.
{"fctl wallets create eryldor"}
You'll get a response like this:
```
You are about to create a wallet.
Do you want to continue [Y/n]: y
SUCCESS Wallet created successfully with ID: 9d21fd84-xxxx-yyyy-zzzz-250aaa73b374
```
Wallets come with a default balance named `main`. You can use it to store and manage assets without creating additional balances. Here, you'll use the `main` balance to store and manage the coins and experience points of Eryldor.
### Create the `season-2024` balance
Now you'll create a balance named `season-2024` to store the badges of Eryldor. This balance will expire at the end of the tournament season 2024. Here it will expire on January 1st, 2025.
Feel free to adapt the expiration date to your needs.
" }} body={{ name: "season-2024", expiresAt: "2025-01-01T00:00:00Z" }}>
{"fctl wallets balances create season-2024 --name eryldor --expires-at \"2025-01-01T00:00:00Z\""}
You'll get a response like this:
```
SUCCESS Balance created successfully with name: season-2024
```
### Inspecting Eryldor's wallet
You can inspect the wallet in the Formance Console.
First open the console.
```bash
fctl ui
```
Then navigate to the Wallets section and select the `eryldor` wallet. You'll see the `main` balance and the `season-2024` balance as shown in the screenshot below.
A common use case for balances is to model vouchers, coupons, or any kind of assets that have a limited lifecycle. For example, you can create a balance for each voucher type, and set the expiration date to the end of the voucher validity period.
## Crediting Eryldor with coins and experience points
After tremendous adventures, Eryldor managed to complete their first quest! As a well-deserved reward, they earned 10 COIN and 150 experience points.
You'll use the `fctl wallets credit` command to credit Eryldor's wallet with these assets.
When you credit a wallet, you must specify where the funds come from. Here, you'll specify that the funds come from the account `world`. `world` is a special account that represents the external world, and is used to introduce or remove assets from the system.
Note that you didn't specify a balance. When you don't specify a balance, the `main` balance is used by default as it is the primary balance of the wallet.
**Add the coins**
" }} body={{ amount: { asset: "COIN", amount: 10 }, sources: [{ type: "account", identifier: "world" }] }}>
{"fctl wallets credit 10 COIN --name eryldor --source account=world"}
**Add the experience points**
" }} body={{ amount: { asset: "XP", amount: 150 }, sources: [{ type: "account", identifier: "world" }] }}>
{"fctl wallets credit 150 XP --name eryldor --source account=world"}
If you specify a source different from `world`, you'll get an `INSUFFICIENT_FUND` error. This is because you can only credit a wallet with assets coming from either the external world or another wallet.
### Inspecting Eryldor's wallet
Let's inspect Eryldor's wallet in the Formance Console.
You'll see that the `main` balance contains 10 COIN and 150 XP, as shown in the screenshot below.
## Debiting Eryldor's wallet
Eryldor is now ready to buy a new spellbook! They found a rare spellbook for 5 COIN! It is a very good deal, so they decide to buy it.
You'll use the `fctl wallets debit` command to debit Eryldor's wallet with 5 COIN.
" }} body={{ amount: { asset: "COIN", amount: 5 }, destination: { type: "account", identifier: "world" } }}>
{"fctl wallets debit 5 COIN --name eryldor --destination account=world"}
You'll get a response like this:
```
SUCCESS Wallet debited successfully!
```
As we did for the credit, you must specify the destination of the funds. Here, you specify that the funds go to the account `world`. `world` being a special account that represents the external world, it can also be used to remove assets from the system.
### Inspecting Eryldor's wallet
Let's inspect Eryldor's wallet in the Formance Console.
You'll see that the `main` balance contains 5 COIN and 150 XP, as shown in the screenshot below.
## Earning badges
Eryldor is a very skilled mage and they are participating in the tournament season 2024. Thanks to their new spellbook, they managed to win the first tournament and earned a badge! Badges are earned in tournaments and expire at the end of the tournament season.
You'll use the `fctl wallets credit` command to credit Eryldor's wallet with a badge. It's a special asset that you'll represent with the `BADGE` currency. The process is the same as for the coins and experience points, with the difference that you'll specify the `season-2024` balance.
" }} body={{ amount: { asset: "BADGE", amount: 1 }, balance: "season-2024", sources: [{ type: "account", identifier: "world" }] }}>
{"fctl wallets credit 1 BADGE --name eryldor --balance season-2024 --source account=world"}
You'll get a response like this:
```
SUCCESS Wallet credited successfully!
```
### Inspecting Eryldor's wallet
Let's inspect Eryldor's wallet in the Formance Console.
You'll see that the `season-2024` balance contains 1 BADGE, as shown in the screenshot below.
## Next steps
In this section, you've learned how to create a wallet, create balances, credit and debit a wallet, and credit a balance. You've also learned how to inspect a wallet in the Formance Console.
In the next session, you'll learn how to use the Wallets to model a hold, a mechanism to freeze assets for a specific purpose, such as a deposit or a reservation.
---
## Managing holds
Source: https://docs.formance.com/modules/wallets/managing-holds
## What is a hold?
A **hold** is a temporary reservation of funds. It's a way to ensure that funds are available when you need them. When you place a hold on a wallet, the funds are not available for spending, but they are still part of the wallet's balance.
After being placed, a hold can be **confirmed**, or it can be **canceled**. When a hold is confirmed, the funds are no longer available for spending and are transferred to the recipient's wallet. When a hold is canceled, the funds are released back to the wallet's balance.
## Placing a hold
While reading their brand new spellbook [acquired earlier](/modules/wallets/basic-operations), Eryldor found the spell they were looking for. However, they need to buy an extra component to cast it. As it is a rare component, the only way they can get it is by participating in an auction.
Fortunately, at the auction-house, Eryldor found the component they need. They placed a bid of 2 COIN, and the auctioneer placed a hold on Eryldor's wallet to ensure that the funds are available when the auction ends, if Eryldor wins.
You'll act as the auctioneer and place a hold on Eryldor's wallet.
With `fctl`, you place a hold using the `wallets debit` command, as if you were debiting the wallet. To create a hold rather than a debit, you use the `--pending` flag.
" }} body={{ amount: { asset: "COIN", amount: 2 }, pending: true }}>
{"fctl wallets debit 2 COIN --pending --name eryldor"}
You should see the following output:
```bash
You are about to debit a wallets.
Do you want to continue [Y/n]: y
SUCCESS Wallet debited successfully with hold id '1f0dbe17-4be4-4db0-ab85-72404c059e73'!
```
### Inspecting the hold
Let's inspect Eryldor's wallet in the Formance Console.
Open the Formance Console and navigate to the Wallets page. You should see a new hold on Eryldor's wallet.
```
fctl ui
```
Originally, assuming you've followed the previous tutorials, Eryldor's wallet had a balance of 5 COIN. After placing the hold, the wallet's balance became 3 COIN, and the hold was created with an amount of 2 COIN.
Now, should Eryldor win the auction? You choose!
## Confirming the hold
After the auction ends, Eryldor won the bid. The auctioneer will take the money from the hold placed earlier on Eryldor's wallet.
To confirm the hold, you use the `fctl wallet hold confirm` command.
" }} body={{ final: true }}>
{"fctl wallets holds confirm --final"}
## Canceling the hold
If Eryldor didn't win the auction, the hold can be canceled to release the funds back to the wallet's balance.
To cancel the hold, you use the `fctl wallet hold void` command.
" }}>
{"fctl wallets holds void "}
## Partially confirming the hold
In some cases, you may want to confirm only a portion of the hold.
For example, Eryldor is ready to pay up to 4 coins for the auction. The auctioneer placed a hold of 4 coins on Eryldor's wallet. However, Eryldor won the auction with a bid of 3 coins. The auctioneer can confirm only 3 coins and cancel the remaining 1 coin.
First, you'll put a hold of 4 coins on Eryldor's wallet.
" }} body={{ amount: { asset: "COIN", amount: 4 }, pending: true }}>
{"fctl wallets debit 4 COIN --pending --name eryldor"}
You should see the following output:
```bash
You are about to debit a wallets.
Do you want to continue [Y/n]: y
SUCCESS Wallet debited successfully with hold id '3af573f9-4c92-490b-9919-4848bf51fa33'!
```
If your wallet doesn't have enough funds, you can top it up using:
```bash
fctl wallets credit "" COIN --name eryldor --source account=world
```
Now, you'll confirm only 3 coins of the hold.
" }} body={{ amount: 3, final: true }}>
{"fctl wallets holds confirm --amount 3 --final"}
You should see the following output:
```bash
SUCCESS Hold '3af573f9-4c92-490b-9919-4848bf51fa33' confirmed!
```
## Summary
In this section, you learned how to manage holds with Formance Wallets. You placed a hold on a wallet, confirmed the hold, canceled the hold, and partially confirmed the hold. You also learned how to inspect a wallet's holds in the Formance Console.
---
## Creating Wallets
Source: https://docs.formance.com/modules/wallets/creating
Create a wallet using the API. The response returns a blank wallet object you can store the reference of in your system.
{"fctl wallets create my-wallet"}
**Response:**
```json
{
"id": "e21494fe-dbd1-4323-8f2c-28c3bafb96d1",
"balances": {},
"metadata": {}
}
```
At this point, the wallet will start to exist on the underlying ledger, but it will not have any balances.
## Choosing a Wallet Strategy
There are multiple strategies you can adopt when creating wallets: you can choose to create a wallet for a specific user, or you can create a wallet for a specific resource in your system.
## Using metadata
Wallets carry a `metadata` field that can be used to store any information you want to associate with the wallet. This is useful when you want to e.g. attach a reference to a resource in your system to the wallet.
{"fctl wallets create user-wallet --metadata user_id=1234"}
---
## Adding funds
Source: https://docs.formance.com/modules/wallets/adding-funds
Add funds to a wallet by issuing a credit via the API. The credit is applied to the wallet's balance immediately.
All amounts use [Universal Monetary Notation](/modules/numscript/monetary-notation) — `USD/2` means US dollars with 2 decimal places, so `100` = $1.00.
## Credit a wallet
The simplest credit — add $1.00 (100 in USD/2) from the default `world` source:
" }}
body={{ amount: { asset: "USD/2", amount: 100 }, sources: [{ type: "account", identifier: "world" }] }}>
{"fctl wallets credit 100 USD/2 --name my-wallet --source account=world"}
## Funding from a specific account
You can fund a wallet from any ledger account — not just `world`. Set the `sources` field to reference the account:
" }}
body={{ amount: { asset: "USD/2", amount: 100 }, sources: [{ type: "account", identifier: "orders:1234" }] }}>
{"fctl wallets credit 100 USD/2 --name my-wallet --source account=orders:1234"}
The source account must have sufficient funds. If it doesn't, the credit will fail with an `INSUFFICIENT_FUND` error.
## Funding from another wallet
You can also fund a wallet from another wallet by using the `wallet` source type:
" }}
body={{ amount: { asset: "USD/2", amount: 100 }, sources: [{ type: "wallet", identifier: "" }] }}>
{"fctl wallets credit 100 USD/2 --name my-wallet --source wallet="}
## Funding a specific balance
By default, credits go to the `main` balance. To credit a named balance (e.g. a voucher or seasonal balance), include the `balance` field:
" }}
body={{ amount: { asset: "COIN", amount: 50 }, balance: "season-2024", sources: [{ type: "account", identifier: "world" }] }}>
{"fctl wallets credit 50 COIN --name my-wallet --balance season-2024 --source account=world"}
---
## Spending funds
Source: https://docs.formance.com/modules/wallets/spending-funds
Spend funds from a wallet by issuing a debit. The debit reduces the wallet's balance immediately.
## Basic debit
Debit $1.00 from a wallet. By default, funds go to the `world` account (representing the outside world):
" }}
body={{ amount: { asset: "USD/2", amount: 100 } }}>
{"fctl wallets debit 100 USD/2 --name my-wallet"}
## Setting the destination
Send debited funds to a specific ledger account instead of `world`:
" }}
body={{ amount: { asset: "USD/2", amount: 100 }, destination: { type: "account", identifier: "orders:1234" } }}>
{"fctl wallets debit 100 USD/2 --name my-wallet --destination account=orders:1234"}
## Sending to another wallet
You can also send funds directly to another wallet:
" }}
body={{ amount: { asset: "USD/2", amount: 100 }, destination: { type: "wallet", identifier: "" } }}>
{"fctl wallets debit 100 USD/2 --name my-wallet --destination wallet="}
The wallet must have sufficient funds. If it doesn't, the debit fails with an `INSUFFICIENT_FUND` error.
---
## Holding and confirming
Source: https://docs.formance.com/modules/wallets/holding-confirm
In some cases you want to reserve funds before confirming the debit — for example, holding funds while an order is being shipped, then confirming once it's delivered.
## Create a hold
Issue a debit with `pending: true` to create a hold. The funds are reserved but not yet spent:
" }}
body={{ amount: { asset: "USD/2", amount: 100 }, pending: true }}>
{"fctl wallets debit 100 USD/2 --pending --name my-wallet"}
The response includes a `hold` field with the hold ID. Use this ID to confirm or void the hold.
## Confirm the hold
Once the event is confirmed (e.g. order shipped), confirm the hold to finalize the debit:
" }} body={{ final: true }}>
{"fctl wallets holds confirm --final"}
## Void the hold
If the event doesn't happen (e.g. order cancelled), void the hold to release the funds back to the wallet:
" }}>
{"fctl wallets holds void "}
## Partial confirmation
You can confirm only part of a hold. For example, if you held $1.00 but only need to charge $0.75:
" }} body={{ amount: 75, final: true }}>
{"fctl wallets holds confirm --amount 75 --final"}
The remaining $0.25 is automatically released back to the wallet when `final: true` is set.
Without `final: true`, partial confirmations leave the hold open — you can confirm additional amounts later until you finalize it.
---
## Flows
Source: https://docs.formance.com/modules/flows
Flows is an integrated service that lets you quickly set up end-to-end money flows, without the headache of piecing together APIs and weaving together complex system interconnections.
With a unified compatibility model, you can easily move value between different ledgers, wallets, and payment processors. Plus, Formance Flows takes care of translating and interpreting the transactions for you.
On top of that, Formance Flows comes with flexible workflow capabilities, so you can create complex flows that account for delays or external events, as well as retry and fallback options.
## Examples
Learn how to initiate a Stripe Connect transfer, sourcing funds from a Formance ledger account.
Learn how to automate the deposit of funds on a user wallet once a payment is received.
Learn how to move funds between accounts existing on different ledgers using Flows.
---
## Workflows definition
Source: https://docs.formance.com/modules/flows/definition
Within the Flows service, a _Workflow_ refers to a template that defines an ordered sequence of actions, called _stages_, to be executed as a Workflow _Instance_.
## Workflow Definition Syntax
A workflow definition is a YAML file, with the following structure:
```yaml
---
name: "my-workflow"
stages:
- send:
# ...
- wait_event:
event: deposit.confirmed
- delay:
seconds: 7d
- send:
# ...
```
## Available stages
Currently, the following stages are supported:
| Stage | Description |
|------------|-----------------------------------------------------------------------------------------------------------------|
| `send` | Transfers value from a source to a destination. This stage is compatible with ledgers, payments, and wallets. |
| `wait_event` | Makes the workflow instance wait for an event to be emitted by an external system. |
| `delay` | Makes the workflow instance wait for a given amount of time. |
To learn more about each stage, please refer to the dedicated documentation page in the following section.
## Using variables
Variables can be used in the workflow definition, by using the `${}` syntax. Variables will then be expanded according to the values passed at workflow execution time.
```yaml
---
stages:
- wait_event:
event: "${event}"
```
## Lifecycle
Workflows have a lifecycle of their own, and need to be created before they can be executed as instances - follow on to the next section to learn more and create your first workflow.
---
## Workflows execution
Source: https://docs.formance.com/modules/flows/execution
## Creating a Workflow
Before running a workflow, we need a definition file. Let's begin by creating a simple one with the YAML below.
```yaml
---
name: "my-workflow"
stages:
- send:
source:
account:
id: "world"
ledger: "flows-demo-001"
destination:
account:
id: "deposits:${depositID}"
ledger: "flows-demo-001"
amount:
amount: 100
asset: "JPY"
```
Let's save this file as `my-workflow.yaml`. We can now create the workflow using the following command:
Executing the above command will save this workflow, and return an ID that can be used to execute it as a workflow instance. The output of the above command should look like this:
```
[SUCCESS] Workflow created with ID: e6415ff5-1d83-4853-998a-cac09ae1513c
```
For the sake of learning the available commands, let's verify that the workflow was successfully saved by listing all our created workflows:
## Executing a Workflow
Alright; we have now created our first workflow, but nothing has happened yet within the ledger `flows-demo-001` that we used in the workflow definition. Let's jump straight to the fun part and execute our workflow as a workflow instance. We can do so using the following command:
" }}
body={{ variables: { depositID: "1234" } }}>
{"fctl orchestration workflows run \\\n --variable depositID=1234"}
## Checking a Workflow instance status
Workflow instances are long-lived. Their current state of execution and termination can be checked using the following command:
## Debugging a Workflow instance
If you're having trouble understanding what a workflow instance is currently doing, you can use the following command to get a detailed view of its current internal state:
```shell
fctl orchestration instances describe dff3791d-b82c-4ed5-bf35-e954872cd2af
```
### Using the API for debugging
For more detailed debugging, you can use the API endpoints directly:
**Get instance history:**
**Get specific stage history:**
These endpoints provide detailed information about:
- The sequence of events that occurred during execution
- Input and output values at each stage
- Error messages and stack traces for failed stages
- Timestamps for each operation
## Terminating a Workflow instance
There are cases where you might want to terminate a workflow instance, e.g. when you want to recreate it after making changes to definition or restart it after a failure with different variables.
If you want to do so, you can simply use the following command:
---
## Triggers
Source: https://docs.formance.com/modules/flows/triggers
A trigger is a way to fire a workflow from a payment event. The trigger is linked to a unique workflow that will be executed only if its filter condition is satisfied. If so, it will forward values from the event to the workflow in a set of configured variables corresponding to the ones expected in the workflow.
## Creating a trigger
To create a trigger, use the [Create Trigger](/stack-api-reference/orchestrationv2/create-trigger) endpoint:
{"fctl orchestration triggers create SAVED_PAYMENT efxxxxx-xxxx-yyyy-dddd-d236abzzzzzz \\\n --filter 'event.type == \"PAY-IN\" && event.provider == \"ADYEN\" && hasPrefix(event.rawData.merchantReference, \"test\") == true' \\\n --vars paymentID=event.id \\\n --vars amount=event.amount \\\n --vars asset=event.asset \\\n --vars merchantID=\"'001'\" \\\n --vars userID=\"'003'\" \\\n --vars merchantReference=event.rawData.merchantReference"}
### Filter syntax
The syntax for the filter is based on the [expr-lang expression language](https://expr-lang.org/docs/language-definition).
Example filter:
```
event.type == "PAY-IN" && event.provider == "PROVIDERID" && hasPrefix(event.rawData.merchantReference, "test") == true
```
## Testing a trigger
Before deploying a trigger to production, you can test it to verify that the filter matches correctly and that variables are extracted as expected.
Use the [Test Trigger](/stack-api-reference/orchestrationv2/test-trigger) endpoint. The payload should be the payment event you want to test against:
" }}
body={{ id: "dummyValue", type: "PAY-IN", asset: "EUR/2", amount: 4199, scheme: "visa", status: "SUCCEEDED", rawData: { amount: { value: 4199, currency: "EUR" }, reason: "012789:0000:03/2030", success: "true", eventCode: "AUTHORISATION", eventDate: "2023-12-15T15:22:32+01:00", operations: ["CANCEL", "CAPTURE", "REFUND"], pspReference: "XXXXXXXX", paymentMethod: "visa", additionalData: { authCode: "789789", expiryDate: "03/2030", cardSummary: "0000" }, merchantReference: "XXXX", merchantAccountCode: "XXXXX" }, metadata: {}, provider: "PROVIDERID", createdAt: "2023-12-15T15:22:32+01:00", reference: "XXXXXX", connectorId: "connectorID", initialAmount: 4199 }} noFctl />
The response shows whether the filter matched and the extracted variable values:
```json
{
"data": {
"filter": {
"match": true
},
"variables": {
"amount": { "value": "4199" },
"asset": { "value": "EUR/2" },
"merchantID": { "value": "001" },
"paymentID": { "value": "dummyValue" },
"userID": { "value": "003" }
}
}
}
```
## Evaluating metadata in triggers
You can use `link()` and `get()` functions to retrieve metadata from related accounts in your trigger variables and filters.
### Accessing account metadata
To retrieve metadata from a payment's associated account:
```json
{
"event": "SAVED_PAYMENT",
"workflowID": "xxx",
"vars": {
"myVar": "get(link(event, \"destination_account\").metadata, \"foo\")"
}
}
```
This example retrieves the `foo` metadata field from the destination account linked to the payment event.
## Webhooks
You can create webhooks to get notified of Flows events, whether they succeed or fail:
| Event | Description |
|-------|-------------|
| `STARTED_WORKFLOW` | Workflow instance has started |
| `SUCCEEDED_WORKFLOW` | Workflow instance completed successfully |
| `FAILED_WORKFLOW` | Workflow instance failed |
| `STARTED_WORKFLOW_STAGE` | A workflow stage has started |
| `SUCCEEDED_WORKFLOW_STAGE` | A workflow stage completed successfully |
| `FAILED_WORKFLOW_STAGE` | A workflow stage failed |
| `SUCCEEDED_TRIGGER` | Trigger fired successfully |
| `FAILED_TRIGGER` | Trigger failed to fire |
You can find the complete list of available events in the [Formance events repository](https://github.com/formancehq/stack/blob/main/libs/events/generated/all.json).
---
## Banking Bridge
Source: https://docs.formance.com/modules/payments/connectors/psp/bankingbridge
The Banking Bridge connector polls a Banking Bridge workspace and surfaces accounts, balances, and payments as read-only streams. It applies the Payments module's most extensive per-payment enrichment: end-to-end IDs, mandate IDs, clearing-system references, creditor info, and remittance fields all land under `com.formance.connectors.bankingbridge.*` on every payment.
Available from Payments 3.3.0. Source: [`ee/plugins/bankingbridge`](https://github.com/formancehq/payments/tree/main/ee/plugins/bankingbridge).
The Banking Bridge connector requires Payments **3.3.0 or higher**. Your stack pins an older version — upgrade to use it.
## Prerequisites
You need a Banking Bridge workspace and a `clientID` + `clientSecret` pair. Banking Bridge uses OAuth2 client-credentials against a separate `authEndpoint`; the connector exchanges the credentials for a short-lived bearer token and refreshes automatically.
## Installation
{"fctl payments connectors install bankingbridge config.json"}
With `config.json` containing:
```json
{
"name": "string",
"clientID": "string",
"clientSecret": "string",
"endpoint": "https://api.bankingbridge.example",
"authEndpoint": "https://auth.bankingbridge.example"
}
```
### Configuration fields
| Field | Required | Default | Description |
|---|---|---|---|
| `name` | yes | — | A unique name for this connector instance. |
| `clientID` | yes | — | Banking Bridge OAuth2 client ID. |
| `clientSecret` | yes | — | OAuth2 client secret. Never logged. |
| `endpoint` | yes | — | Data API base URL. |
| `authEndpoint` | yes | — | OAuth2 token endpoint. Separate from the data endpoint so the connector can reach a dedicated auth host. |
## Capabilities
- `FETCH_ACCOUNTS` — internal accounts via `GET /accounts`, paginated by an opaque cursor + `LastSeenImportedAt` watermark.
- `FETCH_BALANCES` — per-account balance, independent periodic root.
- `FETCH_PAYMENTS` — `GET /transactions`, each row carrying the full enrichment set under `com.formance.connectors.bankingbridge.*`.
Payouts, transfers, bank-account creation, and webhooks are not implemented. Banking Bridge is a read-only observation surface.
## Account model
Every Payments internal account is one Banking Bridge `/accounts` row (a bank account observed via one of the aggregated providers). The `reference` is the Banking Bridge account reference; `name` is the upstream name; `defaultAsset` comes from Banking Bridge's `defaultAsset` field (already UMN-formatted); `createdAt` is the upstream `ImportedAt`. No EXTERNAL accounts are emitted. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Uppercase ISO 4217, formatted to UMN at standard precision. Amounts are already in minor units — no scaling.
## Status mapping
| Banking Bridge transaction status | Payment `status` |
| --- | --- |
| `BOOK`, `BOOKED`, `INFO` (terminal observation) | `SUCCEEDED` |
| `PDNG`, `PENDING` | `PENDING` |
| `RJCT`, `REJECTED` | `FAILED` |
| anything else | `UNKNOWN` |
## Scheme mapping
Banking Bridge tags each transaction with an ISO 20022 triplet (`Domain.Family.SubFamily`). The connector collapses it into the Payments module's `PaymentScheme` + `PaymentType` via [`schemes.go`](https://github.com/formancehq/payments/blob/main/ee/plugins/bankingbridge/schemes.go):
- `PMNT.ICRD` / `PMNT.MCRD` (cards) → `CARD_*`, PAY-IN or PAYOUT per `IssuedX` vs `ReceivedX`.
- `PMNT.ICDT` / `PMNT.RCDT` (credit transfers) → `SEPA` / `WIRE` per sub-family.
- `PMNT.IDDT` / `PMNT.RDDT` (direct debits) → `SEPA_DEBIT`.
- `PMNT.IRCT` / `PMNT.RRCT` (real-time SCT) → `SEPA_INSTANT`.
- Unknown / `MCOP` / `MDOP` → `UNKNOWN`, classified `OTHER`.
## Metadata keys
Banking Bridge fields land under `com.formance.connectors.bankingbridge.*` (not `com.bankingbridge.spec/`) — Banking Bridge is the canonical example of the [payment-reference enrichment pattern](/modules/payments/operations). Operationally useful keys:
- **Account**: `iban`, `bic`, `currency_code`, `account_type`, `provider_name`, `provider_id`, `branch_code`, `imported_at`.
- **Payment** (always emitted when populated by the provider):
- End-to-end identifiers: `end_to_end_id`, `instruction_id`, `transaction_id`, `mandate_id`, `clearing_system_reference`.
- Counterparty info: `debtor_name`, `debtor_iban`, `debtor_bic`, `creditor_name`, `creditor_iban`, `creditor_bic`.
- Creditor reference: `creditor_reference_type`, `creditor_reference_value`, `creditor_reference_issuer`.
- Remittance: `remittance_information_unstructured`, `remittance_information_structured`.
- Bank-side fields: `bank_transaction_code` (the raw `Domain.Family.SubFamily` triplet), `value_date`, `booking_date`, `provider_name`, `provider_id`.
## Workflow tree
```text
FetchAccounts (periodic root)
FetchPayments (periodic root)
FetchBalances (periodic root)
```
Each root holds its own cursor + `LastSeenImportedAt` watermark and advances independently — Banking Bridge's `/transactions` endpoint is workspace-global, not per-account.
## Pagination and recovery
Each capability persists `(cursor, lastSeenImportedAt)` in platform-managed `State`. The `cursor` is opaque; `lastSeenImportedAt` re-seeds the next cycle's lower bound after the current page set is exhausted. Restarts resume from the last committed `State`; the engine dedupes by `PSPPayment.Reference`.
## Known gaps
- **Outbound initiation** (transfers, payouts, bank-account creation) is not implemented.
- **Webhooks** are not wired.
- **Reversals** are not modelled as reversal events — reversed transactions appear as new rows with the `RRTN` / `XRTN` sub-family code.
- **Multi-workspace tenants**: one connector install covers one Banking Bridge workspace.
---
## Send Statement
Source: https://docs.formance.com/modules/flows/stages/send
The `send` statement is the main and most powerful stage of the Workflow definition. It moves funds between ledger accounts, wallets, and payment service providers (PSPs), handling the underlying ledger transactions automatically.
A `send` stage has three attributes:
- `source` — where the funds come from
- `destination` — where the funds go
- `amount` — how much, and which asset
Both `source` and `destination` may reference a ledger account, a wallet, or a payment. Skeleton:
```yaml
---
stages:
- send:
source:
# ...
destination:
# ...
amount:
amount: 100
asset: "EUR/2"
```
## Compatibility matrix
| | ↘ Ledger account | ↘ Wallet | ↘ Payment |
| --- | --- | --- | --- |
| ↗ **Ledger account** | ✅ | ✅ | ✅ |
| ↗ **Wallet** | ✅ | ✅ | ✅ |
| ↗ **Payment** | ✅ | ✅ | ❌ |
Payment destinations work with **any PSP connector configured in the Payments service** — Stripe, Wise, Modulr, Banking Circle, Currency Cloud, Mangopay, and the rest. Validation is delegated to Payments, so new connectors light up here automatically.
| | ↘ Ledger account | ↘ Wallet | ↘ Payment |
| --- | --- | --- | --- |
| ↗ **Ledger account** | ✅ | ✅ | Partial support **[1]** |
| ↗ **Wallet** | ✅ | ✅ | Partial support **[1]** |
| ↗ **Payment** | ✅ | ✅ | ❌ |
**[1]** Payment destinations are currently supported only for the `stripe` connector. Support for the rest will be added in a later release.
## Source types
### Ledger account
Reference a ledger account by `id` (the address) and `ledger` (the ledger name):
```yaml
source:
account:
id: "users:42"
ledger: "flows-demo-001"
```
For cross-ledger flows you can also customize the intermediate "bridge" account via `throughAccount` and allow it to go negative via `allowOverdraft`:
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | string | yes | — | Ledger account address |
| `ledger` | string | yes | — | Ledger name |
| `throughAccount` | string | no | `"world"` | Intermediate account for cross-ledger or payment flows |
| `allowOverdraft` | bool | no | `false` | Allow unbounded overdraft on the source account |
```yaml
source:
account:
id: "users:123"
ledger: "main"
throughAccount: "liabilities:pending"
allowOverdraft: true
```
### Wallet
Reference a wallet by either `id` or `name`. `balance` selects a specific balance (defaults to `main`):
```yaml
source:
wallet:
id: "22d5de50-b5ef-407d-9a03-9e4fc36356f8"
balance: "main" # optional
```
### Payment
Reference an existing payment (a payin) by `id`:
```yaml
source:
payment:
id: "22d5de50-b5ef-407d-9a03-9e4fc36356f8"
```
The payment is "ingested" — moved into a ledger as an intermediate step — before being transferred to the destination. By default this uses an internal orchestration ledger; you can override the ingestion target:
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | string | yes | — | Payment ID from the Payments service |
| `ledger` | string | no | `"orchestration-000-internal"` | Ledger for payment ingestion |
| `holdingAccount` | string | no | `"payment:{id}"` | Account where ingested funds are held |
| `throughAccount` | string | no | `"world"` | Source account for the ingestion transaction |
| `allowOverdraft` | bool | no | `false` | Allow unbounded overdraft on `throughAccount` |
```yaml
source:
payment:
id: "${paymentID}"
ledger: "main"
holdingAccount: "assets:stripe:held"
throughAccount: "assets:stripe:incoming"
allowOverdraft: true
```
See [Payment ingestion](#payment-ingestion) below for the full picture.
## Destination types
### Ledger account
```yaml
destination:
account:
id: "users:42"
ledger: "flows-demo-001"
```
Same `throughAccount` / `allowOverdraft` options as ledger-account sources — they govern the intermediate account used on cross-ledger transfers:
```yaml
destination:
account:
id: "merchants:456"
ledger: "main"
throughAccount: "assets:incoming"
allowOverdraft: true
```
### Wallet
```yaml
destination:
wallet:
id: "22d5de50-b5ef-407d-9a03-9e4fc36356f8"
```
### Payment
Unlike source payments, destination payments aren't referenced by ID (the payment doesn't exist yet). Instead, you tell the stage which PSP connector to use:
```yaml
destination:
payment:
psp: "stripe"
```
The `send` statement creates a payment initiation against the PSP named in `psp`. Only `stripe` is supported at this version — other connectors will be added in a later release.
```yaml
destination:
payment:
psp: "stripe"
type: "PAYOUT"
sourceAccount: "${sourceAccountID}"
metadata: "stripeConnectID"
```
The full field set:
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `psp` | string | yes | — | PSP connector name (`"stripe"`, `"wise"`, `"modulr"`, …) |
| `type` | string | no | `"TRANSFER"` | `"TRANSFER"` or `"PAYOUT"` |
| `metadata` | string | no | — | Account-metadata key carrying the destination PSP account ID |
| `sourceAccount` | string | no | — | Explicit PSP source account ID for the transfer |
| `connectorID` | string | no | — | Specific connector ID (when multiple connectors of the same type exist) |
| `waitingValidation` | bool | no | `false` | Whether to wait for manual validation before settling |
The ledger or wallet debit happens **before** the PSP transfer is initiated. On PSP failure the workflow is left in a consistent state — funds remain debited from the source until the workflow handles the failure explicitly.
## Cross-ledger transfers
When source and destination live on different ledgers, the `throughAccount` field on each side controls the intermediate "bridge" account used.
### Same ledger (direct transfer)
```yaml
send:
source:
account: { id: "users:123", ledger: "main" }
destination:
account: { id: "merchants:456", ledger: "main" }
```
A single transaction posts on `main` from `users:123` to `merchants:456`.
### Different ledgers (bridge transfer)
```yaml
send:
source:
account:
id: "users:sender"
ledger: "ledger1"
throughAccount: "bridge:outbound"
destination:
account:
id: "merchants:receiver"
ledger: "ledger2"
throughAccount: "bridge:inbound"
allowOverdraft: true
```
Two transactions post:
1. On `ledger1`: `users:sender` → `bridge:outbound`
2. On `ledger2`: `bridge:inbound` → `merchants:receiver`
`bridge:inbound` may need `allowOverdraft: true` if it isn't pre-funded.
## The `allowOverdraft` field
By default the Ledger requires source accounts to have sufficient funds. The special `"world"` account has unbounded overdraft and is used by default for bridge transactions — which is why the cross-ledger example above works without explicit overdraft when the default `world` bridge is in play.
When you point `throughAccount` at a custom account, you may need `allowOverdraft: true` to let the account go negative. The generated Numscript carries an `allowing unbounded overdraft` clause.
| Flow | Transaction | Overdraft applied to |
| --- | --- | --- |
| Account → Payment | `source.id → throughAccount` | `source.id` |
| Payment → Account | `throughAccount → holdingAccount` | `throughAccount` |
| Account → Wallet (cross-ledger) | `source.id → throughAccount` | `source.id` |
| Wallet → Account (cross-ledger) | `throughAccount → destination.id` | `throughAccount` |
| Account → Account (1st tx) | `source.id → sourceThroughAccount` | `source.id` |
| Account → Account (2nd tx) | `destThroughAccount → destination.id` | `destThroughAccount` |
### Example: liability tracking for payouts
```yaml
send:
source:
account:
id: "users:${userID}"
ledger: "main"
throughAccount: "liabilities:payouts-pending"
allowOverdraft: true
destination:
payment:
psp: "stripe"
type: "PAYOUT"
```
Posts as `users:{userID}` → `liabilities:payouts-pending` instead of `users:{userID}` → `world`, so the in-flight payout shows up on your liabilities account until the PSP confirms settlement.
## Payment ingestion
When the source is a payment, the funds are ingested into a ledger before being transferred to the destination. By default this happens on an internal orchestration ledger:
```
1. world → payment:{paymentID} on orchestration-000-internal
2. payment:{paymentID} → world on orchestration-000-internal (with metadata)
3. world → destination on the destination ledger
```
To ingest directly into your own ledger and account scheme:
```yaml
source:
payment:
id: "${paymentID}"
ledger: "main"
holdingAccount: "assets:stripe:held"
throughAccount: "assets:stripe:incoming"
allowOverdraft: true
```
Produces:
```
1. assets:stripe:incoming → assets:stripe:held on main
2. assets:stripe:held → destination on main
```
## Complete examples
```yaml
name: "payout-with-tracking"
stages:
- send:
source:
account:
id: "users:${userID}"
ledger: "main"
throughAccount: "liabilities:payouts-pending"
allowOverdraft: true
destination:
payment:
psp: "${psp}"
type: "PAYOUT"
sourceAccount: "${sourceAccountID}"
amount:
amount: "${amount}"
asset: "${asset}"
```
```yaml
name: "payin-custom-ingestion"
stages:
- send:
source:
payment:
id: "${paymentID}"
ledger: "main"
holdingAccount: "assets:stripe:pending"
throughAccount: "assets:stripe:bridge"
allowOverdraft: true
destination:
account:
id: "revenue:${merchantID}"
ledger: "main"
amount:
amount: "${amount}"
asset: "${asset}"
```
```yaml
name: "cross-ledger-transfer"
stages:
- send:
source:
account:
id: "users:${userID}"
ledger: "users-ledger"
throughAccount: "bridge:to-merchants"
destination:
account:
id: "merchants:${merchantID}"
ledger: "merchants-ledger"
throughAccount: "bridge:from-users"
allowOverdraft: true
amount:
amount: "${amount}"
asset: "${asset}"
```
---
## Waiting for events
Source: https://docs.formance.com/modules/flows/stages/wait-event
Using the `wait_event` stage, you can wait for an event to be submitted to a running workflow instance before continuing. This is useful for workflows that require user input, such as a manual approval of a transaction.
```yaml
---
stages:
- wait_event:
event: "payout.confirmed"
```
Events can be then submitted to running workflow instances using the API or fctl:
---
## Waiting for a delay
Source: https://docs.formance.com/modules/flows/stages/delay
The `delay` stage is used to wait for a specific duration or until a specific date:
```yaml
---
stages:
- delay:
duration: 60s
```
```yaml
---
stages:
- delay:
#
until: 2023-06-01T00:00:00Z
```
---
## Ledger to Ledger
Source: https://docs.formance.com/modules/flows/examples/ledger-to-ledger
In the Formance Ledger world, ledgers are logically separated from one another.
Sometimes you need to transfer value from one ledger to another.
Fortunately for us, Flows provides a way to do this that takes care of the underlying details for us.
## Workflow definition
```yaml
---
stages:
# let's first provision a user account on our first ledger
- send:
source:
account:
id: "world"
ledger: "flows-demo-001"
destination:
account:
id: "users:42"
ledger: "flows-demo-001"
amount:
asset: "USD/2"
amount: 100
# now let's transfer that value to our second ledger
- send:
source:
account:
id: "users:42"
ledger: "flows-demo-001"
destination:
account:
id: "users:42"
# note that we're sending to a different ledger
ledger: "flows-demo-002"
amount:
asset: "USD/2"
amount: 100
```
## Running the workflow
Let's save the above workflow to a file called `ledger-to-ledger.yaml` and create it:
Now run the workflow:
" }} />
We can now check the status of the workflow instance:
" }} />
---
## Payment to Wallet
Source: https://docs.formance.com/modules/flows/examples/payment-to-wallet
- At least one payment object synced from the connector
## Workflow definition
```yaml
---
name: "payment-to-wallet-demo"
stages:
- send:
source:
payment:
id: "${paymentID}"
destination:
account:
id: "deposits:${depositID}:pending"
ledger: "flows-demo-001"
amount:
amount: 100
asset: "USD/2"
- wait_event:
event: "deposit.confirmed"
- send:
source:
account:
id: "deposits:${depositID}:pending"
ledger: "flows-demo-001"
destination:
wallet:
id: "${walletID}"
amount:
amount: 100
asset: "USD/2"
```
Let's save this file as `payment_to_wallet.yaml`. We can now create this workflow with the following command:
Now that we have a workflow, we can run a workflow instance with the following command:
" }}
body={{ variables: { paymentID: "ch_1G4Z4p2eZvKYlo2C4q0Z0Z0Z", walletID: "wallet_1G4Z4p2eZvKYlo2C4q0Z0Z0Z", depositID: "deposit_1G4Z4p2eZvKYlo2C4q0Z0Z0Z" } }}>
{"fctl orchestration workflows run \\\n--variable paymentID=ch_1G4Z4p2eZvKYlo2C4q0Z0Z0Z \\\n--variable walletID=wallet_1G4Z4p2eZvKYlo2C4q0Z0Z0Z \\\n--variable depositID=deposit_1G4Z4p2eZvKYlo2C4q0Z0Z0Z"}
Note that we're passing in the payment ID, wallet ID, and deposit ID as variables. These variables are used in the workflow definition to reference the payment, wallet, and deposit objects. You'll need to replace these values with the IDs of the payment and wallet you prepared in the prerequisites.
---
## Routable
Source: https://docs.formance.com/modules/payments/connectors/psp/routable
The Routable connector polls a Routable workspace and surfaces settings accounts, companies (counterparties), payables, and receivables. It also initiates outbound payables in response to Formance `CreateTransfer` and `CreatePayout` workflows.
## Prerequisites
You need a Routable account and an API key. Decide upfront which **team member** will own payables initiated through Formance — Routable requires an `acting_team_member` on every payable. Set it per-connector via config, or per-request via metadata; see [Initiating payouts and transfers](#initiating-payouts-and-transfers).
## Installation
{"fctl payments connectors install routable config.json"}
### Configuration fields
| Field | Required | Default | Purpose |
|---|---|---|---|
| `apiKey` | yes | — | Routable bearer token. Sent as `Authorization: Bearer ` on every request. |
| `endpoint` | no | `https://api.routable.com` | API root. Use `https://api.sandbox.routable.com` for the Routable sandbox. |
| `actingTeamMember` | no | `""` | Default Routable team member ID for payable creation. Optional at the connector level — callers can override per-request via the `com.routable.spec/acting_team_member` metadata key. If neither is set, payable creation fails with a clear validation error before any HTTP call. |
| `pollingPeriod` | no | `30m` | Sync cadence (minimum 20m). |
The connector validates the API key at install with a `GET /v1/settings/accounts?page=1&page_size=1` probe — bad keys fail install rather than the first `FETCH_ACCOUNTS` cycle.
## Capabilities
- `FETCH_ACCOUNTS` — settings accounts via `GET /v1/settings/accounts`.
- `FETCH_BALANCES` — settings-account `available_amount` via `GET /v1/settings/accounts/{id}`.
- `FETCH_EXTERNAL_ACCOUNTS` — companies (counterparties) via `GET /v1/companies`.
- `FETCH_PAYMENTS` — payables and receivables via `GET /v1/payables` and `GET /v1/receivables`.
- `CREATE_TRANSFER` and `CREATE_PAYOUT` — both `POST /v1/payables` (transfers are payables with the `TRANSFER` type).
Webhooks, bank-account creation, and reversals are not implemented.
Routable caps API throughput at roughly 1.5 payouts per second. The connector declares this via `PluginWithPayoutThrottle`, so the platform throttles `CreatePayout` / `CreateTransfer` through a dedicated Temporal task queue. See [Operations → Connector reliability](/modules/payments/operations).
## Account model
Every Payments internal account is one Routable settings account from `GET /v1/settings/accounts`. The `reference` is the settings-account ID; `defaultAsset` is the account currency. EXTERNAL accounts come from `/v1/companies` (the payee surface), refreshed on a 24-hour cadence to respect upstream rate limits. See [Accounts](/modules/payments/accounts) for the cross-connector model.
## Asset model
Routable returns ISO currency codes (`USD`, `EUR`, `KWD`). Formatted to UMN via `currency.FormatAsset` — `USD/2`, `EUR/2`, `KWD/3`. Unsupported currencies are skipped (logged) without dropping the rest of the page. Amounts arrive as decimal strings and convert to integer minor units with half-up rounding.
## Status mapping
Routable payable statuses → Payments module payment statuses:
| Routable `status` | Payment `status` |
| --- | --- |
| `draft`, `ready_to_send`, `pending`, `scheduled`, `initiated`, `processing`, `in_transit`, `awaiting_delivery` | `PENDING` |
| `completed`, `paid`, `externally_paid`, `delivered` | `SUCCEEDED` |
| `failed`, `returned`, `nsf` | `FAILED` |
| `stopped`, `canceled`, `cancelled`, `voided` | `CANCELLED` |
| `expired` | `EXPIRED` |
| anything else (or empty) | `UNKNOWN` |
Comparison is case-insensitive. Payment `scheme` is mapped from `delivery_method`: `ach_*` → `ACH`, everything else → `OTHER`.
## Metadata keys
Under `com.routable.spec/`. Full list in the connector's `MAPPINGS.md`; highlights:
### Account metadata (internal accounts)
`object`, `type`, `is_valid`, `currency_code`, plus `type_details.account_type`, `type_details.bank_name`, `type_details.account_number`, `type_details.routing_number`.
### External-account metadata (companies)
`object`, `type`, `status`, `country_code`, `is_vendor`, `is_customer`, `is_archived`, `external_id`, `business_name`, `display_name`, plus the structured `registered_address.*` keys.
### Payment metadata
For payables (PAYOUT) and receivables (PAYIN): `type`, `delivery_method`, `status`, `external_id`, `memo`, `reference`. Two correlation aliases also land on every Formance-initiated Payment:
- `com.routable.spec/payment_initiation_reference` — the originating PaymentInitiation reference (absent on payables created in Routable's UI).
- `com.routable.spec/payable_id` — the Routable payable UUID (mirrors `Payment.Reference`).
See [Correlating an initiation with the synced payment](#correlating-an-initiation-with-the-synced-payment).
## Initiating payouts and transfers
`CreateTransfer` and `CreatePayout` translate the Formance `PSPPaymentInitiation` into a `POST /v1/payables`. Most fields come from the structured initiation; a few Routable knobs are exposed via metadata:
| Metadata key | Required | Default | Maps to | Purpose |
|---|---|---|---|---|
| `com.routable.spec/type` | no | `ach` | `type` | Payable rail (`ach`, `wire`, `check`, `international`, `external`, `vendor_choice`). |
| `com.routable.spec/delivery_method` | no | `ach_standard` | `delivery_method` | Specific delivery option (`ach_standard`, `ach_same_day`, `wire`, `check`, …). Must be compatible with `type`. |
| `com.routable.spec/acting_team_member` | conditional | connector config | `acting_team_member` | Routable team member ID initiating the payable. Required at request time — from either the connector config or this key. |
| `com.routable.spec/external_id` | no | `""` | `external_id` | Caller-supplied external reference (idempotent lookup key on Routable's side). |
| `com.routable.spec/line_item_description` | no | `PSPPaymentInitiation.Description`, then `"Payment "` | `line_items[0].description` | Description on the auto-generated single-line item. Routable v1 requires a non-empty value. |
The Formance `PaymentInitiation.Reference` is sent as the `Idempotency-Key` header — Routable returns the original payable on retries with the same key, which Formance's create-then-poll workflow relies on.
### Async response handling
`POST /v1/payables` answers in two shapes:
| Routable response | Behavior |
|---|---|
| `202 Accepted` (async) | `PollPayoutStatus` / `PollTransferStatus` scheduled against `GET /v1/payables/{id}`; the first successful poll links the PaymentInitiation to a Payment and ends the loop. |
| `201 Created` with a **terminal** status (`completed`, `failed`, `cancelled`, `expired`) | Payment returned immediately. |
| `201 Created` with a **non-terminal** status | Polling round scheduled; first poll returns the Payment. |
Once linked, further transitions (PENDING → PROCESSING → SUCCEEDED) flow through the periodic `FETCH_PAYMENTS` schedule.
### Correlating an initiation with the synced payment
Initiating a payable through Formance produces two rows:
- A `PaymentInitiation` keyed by the reference you supplied (e.g. `payout-acmecorp-20260506-172725`).
- A `PSPPayment` keyed by Routable's payable UUID.
They're linked at the engine level. To resolve one from the other:
```bash
curl "$STACK/api/payments/v3/payment-initiations/$PI_ID/payments" \
-H "Authorization: Bearer $TOKEN" | jq '.cursor.data[] | {reference, status}'
```
Formance-initiated Payments carry the originating reference under `com.routable.spec/payment_initiation_reference` (and its Routable alias `com.routable.spec/external_id`). Payments created in Routable's UI don't have these keys.
```bash
curl -s "$STACK/api/payments/v3/payments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"$match": {"connectorID": "'$CONNECTOR_ID'"}}' \
| jq '.cursor.data[] | {
payment_ref: .reference,
pi_ref: .metadata."com.routable.spec/payment_initiation_reference",
payable_id: .metadata."com.routable.spec/payable_id"
}'
```
## Pagination and recovery
List endpoints are 1-indexed `page` + `page_size` (cap 100). The connector checkpoints pagination opaquely between cycles, so a mid-cycle worker crash resumes deterministically — no row double-billed or dropped.
The payments fetcher walks payables, then receivables, then advances its cycle watermark. The watermark stays **immutable for the full duration of a cycle** to avoid the page-2-tighter-than-page-1 race that would drop rows whose `status_changed_at` lands between page boundaries.
Routable's `status_changed_at.gte` filter is inclusive, so cycle-boundary rows re-emit each cycle. The engine dedupes by `PSPPayment.Reference` — wasted bandwidth, never a correctness issue.
## Known gaps
- **Webhooks** — Routable exposes a webhook product upstream; the connector uses polling. Subscription shape in upstream `MAPPINGS.md §6.4`.
- **Bank-account creation** — companies and delivery methods are pulled, not created. Manage new companies in Routable's UI.
- **Reversals** — `ReverseTransfer` and `ReversePayout` are not wired.
- **Throughput cap** — Routable caps API throughput at ~1.5 payouts/s. The platform throttles outbound workflows via a dedicated task queue; bursts queue under load rather than fail.
---
## Ledger to Stripe Payout
Source: https://docs.formance.com/modules/flows/examples/ledger-to-payout
In this example, we're going to create a workflow that will transfer funds from a ledger account to a Stripe Connect account by leveraging the `send` stage.
To initiate a transfer from a ledger account to a Stripe Connect account, you must have set the `formanceAccountID` metadata key on the ledger account metadata to identify the formance account ID related to the Stripe Connect account to initiate the transfer to.
## What's happening in this example?
Before we dive into the workflow definition, let's take a look at what's happening in this example. As you may imagine, when we say "transfer funds from a ledger account to a Stripe Connect account", we don't mean that funds are actually being teleported from our own ledger account to our Stripe Connect account balance. Instead, we'll be doing two things:
1. Recycle the funds we previously introduced in the ledger by transferring them to the `@world` account
2. Transfer the funds from our main Stripe balance to the target Stripe Connect account
```mermaid
graph TD
A["payouts:1234"] -->|"100 USD/2"| B["world"]
C["stripe main balance"] -->|"100 USD/2"| D["stripe connect account (acct_xxx)"]
A -.->|"flows orchestration"| D
```
*Ledger transaction (top flow) and Stripe Connect transfer (bottom flow) orchestrated by Flows*
The Flows service will be taking care of the sequencing for us.
- A ledger account provisioned with funds
- A `formanceAccountID` metadata key set on the account, corresponding to the Stripe Connect account to transfer funds to
## Workflow definition
Here's the workflow definition we'll be using in this example:
```yaml
---
name: "ledger-to-stripe-payout-demo"
stages:
- send:
source:
account:
id: "payouts:1234"
ledger: "flows-demo-001"
destination:
payment:
psp: "stripe"
amount:
amount: "${amount.amount}"
asset: "${amount.asset}"
```
---
## Reconciliation
Source: https://docs.formance.com/modules/reconciliation
For the mental model of how Reconciliation, Ledger, and Payments work together, see [How the Modules Fit Together](/getting-started/modules-fit-together).
Formance Reconciliation continuously checks that your financial state matches the controls you define. It can compare Ledger balances with external cash, verify relationships inside a ledger, and watch account groups for unexpected balances.
When a control fails, Reconciliation preserves the numbers behind the failure, opens a stable alert, and records how your team handled it. This turns reconciliation from a one-off balance check into an operating process:
```mermaid
flowchart LR
Observe["Observescheduled controls"] --> Detect["Detecta failed check"]
Detect --> Alert["Alertthe right team"]
Alert --> Evidence["Reviewfrozen evidence"]
Evidence --> Resolve["Resolveor accept"]
```
## What you can control
- **External backing:** confirm that cash held by a payment provider matches the corresponding Ledger position.
- **Ledger integrity:** prove that two or more groups of Ledger accounts remain in balance.
- **Balance thresholds:** detect when a treasury, suspense, reserve, or operational account group moves outside an expected range.
- **Source parity:** compare any two supported balance sources, including ledger-to-ledger, ledger-to-pool, and pool-to-pool checks.
Controls run on demand or on a cron schedule. Each run produces a persisted evaluation with a `PASS`, `FAIL`, or `ERROR` result. Failures open one alert per affected asset, so different currencies can be investigated and closed independently.
## From detection to closure
An alert remains a single operational case within its reconciliation period. Your team can acknowledge it, temporarily snooze notifications, mark it fixed after corrective bookings, or formally accept the discrepancy with an author and required note. A later passing evaluation closes it automatically.
Every evaluation and manual transition is retained in the alert timeline. This lets finance and compliance teams answer not only *whether* a control failed, but also when it began, what values were observed, who handled it, and why it was closed.
Reconciliation checks aggregate financial state. It does not match individual Ledger postings to individual bank or PSP statement lines.
## Explore Reconciliation
Learn how rules, evaluations, alerts, evidence, and reconciliation periods fit together.
Create a Ledger integrity rule, evaluate it, and inspect the result.
Compare the four control templates and configure their balance sources and tolerances.
Acknowledge, snooze, resolve, or accept a discrepancy without losing its history.
Formance Reconciliation compares balances between your Formance Ledger and [cash pools](/modules/payments/cash-pools) to verify financial consistency and identify discrepancies that need investigation.
You create a policy that selects Ledger accounts and a cash pool. When you run the policy, Reconciliation reads both sides at the requested timestamps and reports whether their per-asset balances match.
```mermaid
flowchart LR
Pool["Cash poolexternal funds"] --> Compare["Reconciliationpolicy"]
Ledger["Ledger accountsrecorded position"] --> Compare
Compare --> Result["OK or NOT_OKwith drift by asset"]
```
Regular checks help you detect discrepancies, prepare financial reports, and retain a history of the balances observed. Start with [Concepts](/modules/reconciliation/concepts), then follow [Getting Started](/modules/reconciliation/getting-started) to create and run a policy.
This version provides synchronous, on-demand policy runs. Rules, scheduled evaluations, managed alerts, and resolution workflows require Reconciliation 2.4.0 or later.
---
## Concepts
Source: https://docs.formance.com/modules/reconciliation/concepts
Reconciliation organizes continuous financial controls into five client-facing concepts: **rules**, **evaluations**, **alerts**, **evidence**, and **resolutions**.
```mermaid
flowchart LR
Rule["Rulewhat and when to check"] --> Evaluation["Evaluationone execution"]
Evaluation -- FAIL --> Alert["Alertone case per asset and period"]
Evaluation -- PASS --> Proof["Green proof"]
Alert --> Timeline["Evidence andevent timeline"]
Alert --> Resolution["Auto-resolve,fix, or accept"]
```
## Rules
A **rule** defines the business control. It contains:
- a control template and its configuration;
- an on-demand or cron **schedule**, controlling how often it runs;
- a severity, and a **period length** for grouping its results;
- optional key-value labels copied to alerts and webhook event payloads;
- an enabled state.
Templates keep the public configuration focused on financial intent. You choose a supported comparison and provide its sources, assets, signs, thresholds, or tolerances; the underlying expression is generated and validated by Reconciliation.
Labels are metadata for downstream consumers; Reconciliation does not interpret them or route work by itself. For example, `{ "team": "treasury", "environment": "production" }` lets your webhook consumer send the resulting alert to the treasury queue while retaining the same tags in its audit record.
Rules are enabled by default. Disabling a rule stops scheduled and on-demand evaluations without deleting its existing evidence or alerts; see [Pause or resume a rule](/modules/reconciliation/getting-started#6-pause-or-resume-a-rule) for the operational effects.
See [Control Templates](/modules/reconciliation/controls) for the available controls.
## Evaluations
An **evaluation** is one execution of a rule. It is always persisted and has one of three results:
| Result | Meaning |
| --- | --- |
| `PASS` | Every checked asset satisfied the control. |
| `FAIL` | At least one asset fell outside the control. |
| `ERROR` | Reconciliation could not complete the check, for example because a source was unavailable. |
For each asset, a passing result stores a compact proof containing the observed balances and predicate inputs needed to verify the result after a rule edit. A failing result stores a fuller evidence breakdown, including the observed values, calculated difference, configured tolerance, and the generated expression used to explain the check. Amount comparisons use arbitrary-precision integers, so large financial values remain exact.
### Point-in-time reads
Each balance source is read at an explicit point in time. By default, Reconciliation subtracts a 30-second **safety margin** from `at`. For example, an evaluation requested at `10:00:00` reads the sources at `09:59:30`.
The margin avoids evaluating the newest edge of the data, where a Ledger write, connector poll, or balance update may still be reaching one source but not another. It reduces false discrepancies caused by ingestion timing; it does not change or delay the underlying financial activity.
Omit `safetyMargin` to use the 30-second default. Set it explicitly to `"0s"` to read exactly at `at`; this explicit zero is preserved on scheduled rules and is not replaced by the default. It is useful for deterministic tests, demos, and historical evaluations where you control the data and need the exact requested instant. You can also override the effective timestamp for an individual source when systems settle on different cycles.
After resolving the balances, Reconciliation evaluates immutable per-asset snapshots without reading the sources again. The resolved timestamps are stored in `pitPerSource` on the evaluation. Together with the frozen proof or evidence, this records the financial state that produced the verdict.
If you omit `at`, Reconciliation asks Payments for its latest known balance. Payments returns the balance but not the time at which that snapshot became effective, so Reconciliation can record only when it fetched the value. Replaying a historical query at that fetch time may therefore return a different snapshot. For a reproducible run, provide a past `at` or an explicit Payments timestamp in `sourcePITs`; regardless of the read mode, the values stored in the evaluation remain unchanged.
## Alerts
A failed evaluation opens an **alert** for each failing fingerprint. In the current templates, the fingerprint is the asset, such as `asset:USD/2`. A USD discrepancy and a EUR discrepancy therefore become separate cases that can resolve independently.
Alerts have three statuses:
```mermaid
stateDiagram-v2
[*] --> OPEN: first failure
OPEN --> ACKNOWLEDGED: operator acknowledges
OPEN --> RESOLVED: passes, fixed, or accepted
ACKNOWLEDGED --> OPEN: later failure resurfaces
ACKNOWLEDGED --> RESOLVED: passes, fixed, or accepted
RESOLVED --> OPEN: fails again in the same period
```
The alert row represents the current state. Its event timeline records every evaluation that touched it and every acknowledgement, snooze, resolution, acceptance, or reopen.
## Reconciliation periods
Each rule groups its results into **reconciliation periods**.
The `periodType` field sets how long a period is: `daily`, `weekly`, `monthly`, or `continuous` for no boundary at all.
The `cadence` field sets how long a period is: `daily`, `weekly`, `monthly`, or `continuous` for no boundary at all.
`periodType` sets the length of a period, not how often the rule runs — that is its [schedule](#rules), a separate field. A rule with an hourly schedule and a `monthly` `periodType` runs hundreds of times in July, and those runs are grouped into one period, `2026-07`, rather than into hundreds of separate ones.
`cadence` sets the length of a period, not how often the rule runs — that is its [schedule](#rules), a separate field. A rule with an hourly schedule and a `monthly` cadence runs hundreds of times in July, and those runs are grouped into one period, `2026-07`, rather than into hundreds of separate ones.
Grouping uses the effective read timestamp rather than the moment the rule fired, so the [safety margin](#point-in-time-reads) decides which side of a boundary a run falls on. With the 30-second default, a run at `2026-07-01T00:00:00Z` reads at `2026-06-30T23:59:30Z` and is therefore grouped under `2026-06`.
**Renamed in 2.5.0.** This field was called `cadence` in earlier versions. Nothing breaks: both names are accepted when creating a rule, and both are returned, so existing integrations keep working untouched.
Sending both is rejected with a `400` unless the two values are equal — rather than silently picking one. `cadence` is deprecated and will be removed in a future major version, so prefer `periodType` in new code.
| Period type | Period | Example period id |
| --- | --- | --- |
| `continuous` | One ongoing case per asset, with no period boundary | `continuous` |
| `daily` | Each UTC day, certified separately | `2026-07-21` |
| `weekly` | Each ISO week | `2026-W30` |
| `monthly` | Each calendar month, for period-end certification | `2026-07` |
Within a period, the same asset reuses its alert. A failure in a new daily, weekly, or monthly period creates a new alert, preserving earlier periods as independent records.
This is what the period length buys you: an hourly rule that keeps failing all month produces one `monthly` alert with a rising `occurrenceCount`, not hundreds of separate cases.
### How period boundaries are drawn
Boundaries are fixed UTC calendar buckets. They are not configurable, and there is no way to define a period by hand.
| | Boundary |
| --- | --- |
| `daily` | The UTC calendar day, midnight to midnight. Always exactly 24 hours: UTC has no daylight saving, so a period is never 23 or 25 hours long. |
| `weekly` | The **ISO 8601** week, which starts on **Monday**, not Sunday. The year in the id is the ISO year, which for a few days around 1 January differs from the calendar year — a run on 1 January 2027 can file under `2026-W53`. |
| `monthly` | The Gregorian calendar month, so the length follows the calendar: 28, 29, 30, or 31 days. Nothing assumes a fixed 30. |
Three things this does **not** support today:
- **A local or per-tenant timezone.** A `daily` period is a UTC day, not your business day. For a team at UTC-5, period `2026-03-15` runs from 19:00 on 14 March to 19:00 on 15 March local time.
- **A period starting anywhere other than midnight.** There is no business-day offset.
- **A fiscal calendar.** No 4-4-5, no 13-period year, and no fiscal year that starts on a date other than 1 January.
`schedule.tz` does **not** move period boundaries. It sets the timezone the cron expression fires in — that is, when the rule *runs*. A rule with `tz: "America/New_York"` runs on New York time and still files its results into UTC periods.
To choose the period a result lands in — backfilling a close, or replaying a past date — evaluate on demand and pass `at`. The period is derived from that instant, less the [safety margin](#point-in-time-reads), so `at` selects the bucket. It must be in the past. Scheduled runs derive their own instant and have no equivalent override.
A period is operationally green when it has no `OPEN` or `ACKNOWLEDGED` alerts. Resolved and accepted cases remain available for audit.
## Resolutions
An alert can close in three ways:
| Resolution | When to use it | Recorded context |
| --- | --- | --- |
| `auto` | A later evaluation passes. | System attribution and timestamp. |
| `fixed_by_booking` | Your team made a corrective booking. | Author, optional note, and optional transaction references. |
| `accepted_by_business` | The discrepancy is understood and approved without a correction. | Author, required note, and a frozen evidence snapshot. |
If the control fails again within the same period, the alert reopens and its current resolution is cleared. The previous resolution remains in the append-only timeline.
## Data discrepancies and execution errors
Financial failures create asset alerts. Failures to execute a rule create a separate `engine.error` alert so operational problems do not look like balance discrepancies. Use the alert labels and fingerprint to route these two classes to different teams.
Reconciliation compares Formance Ledger balances against external payment-system data grouped into [cash pools](/modules/payments/cash-pools).
## Policies
A **policy** identifies the Ledger account set and cash pool to compare. It contains a name, Ledger name, Ledger query, and payments pool ID. Both sets are resolved when the policy runs, so query-based account and pool membership can change over time.
## Reconciliations
A **reconciliation** is one synchronous policy run. You provide separate past timestamps for Ledger and Payments to account for processing or settlement delays. The result stores:
- `status`: `OK` or `NOT_OK`;
- `ledgerBalances` and `paymentsBalances` by asset;
- `driftBalances` for any difference;
- the timestamps used for both sides.
Completed results are immediately available from the reconciliation list and detail endpoints. There is no in-progress state.
## Balance availability
Cash-pool balances are historical observations produced by connector ingestion. If you request a timestamp beyond the latest known historical balance window, the point-in-time response can be empty even though a latest balance exists.
Choose reconciliation timestamps covered by your connector's ingested balance history. Shorter polling periods reduce the gap between external balance observations.
---
## Getting Started
Source: https://docs.formance.com/modules/reconciliation/getting-started
This guide creates a control that verifies two groups of Ledger accounts remain in balance, runs it once, and shows how to inspect a discrepancy.
- Reconciliation 2.4.0 or later and Ledger 2.4.11 or later
- A Ledger containing the accounts you want to check
- API access with `reconciliation:read` and `reconciliation:write`
- Account metadata or address patterns that identify both sides of the control
- The Webhooks module enabled and a [webhook endpoint configured](/modules/webhooks) if alert notifications should be delivered outside Formance
All amounts in this guide use Ledger's smallest units. For `USD/2`, `100` represents USD 1.00.
## 1. Define the financial relationship
Assume your `main` ledger contains:
- asset accounts tagged with `reconciliation.category=held`;
- obligation accounts tagged with `reconciliation.category=obligation`.
The control should verify, per asset, that the two groups net to zero. Each term has a sign so both account sets can be compared regardless of how their balances are represented.
## 2. Create the rule
Create a `ledger_invariant` rule with a strict zero tolerance for USD:
The response includes the rule `id`, its enabled state, and an `explanationCEL` field describing the generated financial check. Save the rule ID for the next request.
Start on demand while validating account selection and sign conventions. After a few correct runs, update the rule to a cron schedule.
## 3. Evaluate the rule
Run the rule at a known historical instant:
" }} noFctl
body={{ at: "2026-07-20T23:59:59Z", safetyMargin: "0s" }} />
This example uses `"0s"`, so every source is read exactly at `2026-07-20T23:59:59Z`. If `safetyMargin` were omitted, the 30-second default would make the effective read time `2026-07-20T23:59:29Z` instead.
In production, the margin avoids the newest edge of the data, where a Ledger write or connector balance update may still be in flight. Use `"0s"` for deterministic tests, demos, or historical runs where the data is already settled and the exact instant matters.
The evaluation returns one outcome for every configured asset. A passing outcome contains a compact proof:
```json
{
"data": {
"id": "",
"ruleID": "",
"result": "PASS",
"pitPerSource": {
"ledger:main#0": "2026-07-20T23:59:59Z",
"ledger:main#1": "2026-07-20T23:59:59Z"
},
"evidence": [
{
"fingerprint": "asset:USD/2",
"passed": true,
"proof": {
"positive": "250000",
"negative": "-250000",
"tolerance": "0"
}
}
]
}
}
```
If the groups differ, `result` is `FAIL`. The failing entry contains the full balance breakdown and an alert is opened for `asset:USD/2`.
## 4. Inspect the alert
List active alerts for the rule using the query builder:
"}},{"$match":{"status":"OPEN"}}]}'} noFctl />
The alert identifies the asset, current evidence, first and last observation times, occurrence count, and reconciliation period. Its `lastEvaluationID` links back to the evaluation that most recently changed it.
For a complete chronology, list the alert's events:
" }} noFctl />
## 5. Move to a schedule
Once the rule selects the right data, update it to run every hour. Its `daily` period length is unaffected; running more often does not create more periods, because the hourly runs are still grouped by UTC day:
" }} noFctl
body={{ schedule: { kind: "cron", expr: "0 * * * *", tz: "UTC", safetyMargin: "30s" } }} />
For a tick scheduled at `10:00:00`, the 30-second margin reads every source at `09:59:30`. This gives recent Ledger writes and Payments ingestion a short window to settle before comparison. It does not wait 30 seconds before running; it moves the financial observation time 30 seconds into the past.
If you omit `safetyMargin`, scheduled evaluations use the 30-second default. To disable the margin, save `safetyMargin: "0s"`; the explicit zero is preserved and every scheduled run reads at its exact occurrence time.
For a `daily` rule, alerts are grouped by the UTC day of the effective read timestamp.
Use a `continuous` `periodType` for one ongoing operational case, or `weekly` and `monthly` for period-based certification.
Use a `continuous` cadence for one ongoing operational case, or `weekly` and `monthly` for period-based certification.
## 6. Pause or resume a rule
Rules are enabled by default. Disable a rule when a control is temporarily not applicable or its sources are under maintenance:
" }} noFctl
body={{ enabled: false }} />
Disabling a rule:
- stops scheduled evaluations and cancels pending jobs for the previous rule revision;
- rejects on-demand evaluation requests while the rule is disabled;
- preserves existing evaluations, alerts, and alert timelines;
- leaves active alerts active—they do not become resolved merely because the rule stopped running.
Resume the rule with:
" }} noFctl
body={{ enabled: true }} />
For a cron rule, Reconciliation calculates the next future occurrence when it is re-enabled. It does not create evaluations for the interval during which the rule was disabled.
## 7. Delete a rule
Rule deletion is available through the API; no manual PostgreSQL operation is required:
" }} noFctl />
A successful deletion returns `204 No Content`. It permanently removes the rule and cascades to all of its evaluations, alerts, and alert-event timelines.
Deletion removes the audit history associated with the rule. Disable the rule instead when you need to stop future evaluations but retain prior evidence and resolution records. Export any required records before deleting.
## Next steps
- Compare this template with the other [control templates](/modules/reconciliation/controls).
- Learn how to [acknowledge, snooze, resolve, and accept alerts](/modules/reconciliation/alerts).
- Use a [cash pool](/modules/payments/cash-pools) to reconcile external provider balances.
This guide creates a policy, runs a synchronous ledger-to-cash-pool reconciliation, and interprets the result.
- A [cash pool](/modules/payments/cash-pools) containing the payment accounts to reconcile
- API access with `reconciliation:read` and `reconciliation:write`
1. Create a policy
" }} />
Save the returned policy `id`.
2. Run the policy
" }}
body={{ reconciledAtLedger: "2026-07-20T23:59:59Z", reconciledAtPayments: "2026-07-20T23:59:59Z" }} />
Both timestamps must be in the past. They can differ when Ledger and the payment provider reflect the same business event at different times.
3. Interpret the result
- `OK` means the balances matched under the policy's sign convention.
- `NOT_OK` means the response contains a non-zero `driftBalances` value or another comparison error.
Use `listReconciliations` and `getReconciliation` in the [Stack API reference](/stack-api-reference) to retrieve previous runs.
Legacy policies remain supported in later versions. New implementations should use rules and evaluations to gain tolerances, scheduling, alert management, and auditable resolution.
---
## Control Templates
Source: https://docs.formance.com/modules/reconciliation/controls
Control templates describe the relationship Reconciliation should protect. Choose the template closest to the business question, then configure its sources and per-asset tolerance.
| Template | Business question | Sources |
| --- | --- | --- |
| `ledger_vs_pool_drift` | Does cash held externally back the position recorded in Ledger? | One Ledger account set and one cash pool |
| `ledger_invariant` | Do several Ledger account groups maintain the required net relationship? | N Ledger account sets as signed terms (at least one, with no upper limit; meaningful net relationships usually use two or more) |
| `account_threshold` | Is a Ledger account group within its allowed balance range? | One Ledger account set |
| `source_parity` | Do two independent balance sources agree? | Any two Ledger account sets or cash pools |
All four templates evaluate one outcome per asset. If two assets fail, Reconciliation opens two alerts so they can be handled independently.
## Template catalog
Use `ledger_vs_pool_drift` for the classic backing check between a dynamic Ledger account set and a [cash pool](/modules/payments/cash-pools).
```json
{
"templateKind": "ledger_vs_pool_drift",
"templateSpec": {
"ledger": "main",
"ledgerQuery": {
"$match": { "metadata[reconciliation.pool]": "stripe" }
},
"paymentsPoolID": "",
"ledgerSign": -1,
"tolerance": {
"USD/2": 0,
"EUR/2": 50
}
}
}
```
The control checks this relationship for every asset found on either side:
```text
absolute(ledgerSign × ledger balance + pool balance) ≤ tolerance
```
Choose the sign from the way your Ledger represents the external position:
- use `-1` when the Ledger and pool balances are both naturally positive;
- use `+1`, the default, when the Ledger balance is the negative counterpart of positive external cash.
Tolerance defaults to zero for assets not listed. This means a newly observed asset is still checked strictly instead of being silently ignored.
When migrating a legacy policy, omit `ledgerSign` to preserve its `ledger + pool = 0` convention. Review the first evaluation's raw and signed Ledger values before scheduling the rule.
Use `ledger_invariant` when signed groups of accounts inside Ledger must preserve a financial identity. Typical examples include customer assets versus obligations, safeguarded funds versus customer entitlements, or a control account versus its sub-ledger accounts.
```json
{
"templateKind": "ledger_invariant",
"templateSpec": {
"terms": [
{
"ledger": "main",
"query": { "$match": { "metadata[funds.role]": "held" } },
"sign": 1
},
{
"ledger": "main",
"query": { "$match": { "metadata[funds.role]": "obligation" } },
"sign": -1
}
],
"tolerance": {
"USD/2": 0,
"EUR/2": 0
}
}
}
```
Each term selects a Ledger account set and applies a sign of `+1` or `-1`. Reconciliation requires at least one term and does not impose an upper limit, although a meaningful net relationship normally uses two or more:
```text
absolute(Σ signᵢ × balanceᵢ) ≤ tolerance
```
A one-term invariant is valid, but it reduces to checking whether that signed balance is within tolerance and therefore overlaps with `account_threshold`.
For every asset listed in `tolerance`, Reconciliation checks the signed sum against that tolerance. Assets not listed are outside this rule's scope.
Use metadata queries when membership is expected to evolve. New accounts matching a term are included automatically at the next evaluation.
Use `account_threshold` to ensure the aggregate balance of an account set remains above a minimum, below a maximum, or inside a range.
```json
{
"templateKind": "account_threshold",
"templateSpec": {
"ledger": "main",
"query": {
"$match": { "metadata[treasury.role]": "operating" }
},
"mode": "aggregate",
"bounds": {
"USD/2": { "min": 100000, "max": 5000000 },
"EUR/2": { "min": 50000 }
}
}
}
```
Each asset needs at least one bound. If both are present, `min` must be less than or equal to `max`.
The current release supports `aggregate` mode: all accounts matching the query are summed before the bounds are checked. To watch one account, make the query select only that address. Per-account fan-out is not part of this release.
Use `source_parity` when two independently maintained balance sources should agree. Either side can be:
- a Ledger account set, identified by a Ledger and query;
- a Payments cash pool, identified by its pool ID.
```json
{
"templateKind": "source_parity",
"templateSpec": {
"left": {
"kind": "ledger",
"ledger": "main",
"query": { "$match": { "address": "control:stripe" } }
},
"right": {
"kind": "payments_pool",
"poolID": ""
},
"scope": "aggregate",
"tolerance": {
"USD/2": 0,
"EUR/2": 50
}
}
}
```
The template checks `absolute(left - right) ≤ tolerance` for the union of assets observed on both sides. A missing asset is treated as zero, ensuring that a one-sided balance is detected.
`aggregate` is the only supported scope in this release. Both Ledger-to-Ledger and pool-to-pool comparisons are supported; each source is still read independently.
## Set tolerances deliberately
Tolerances use integer asset units and must be non-negative. They are useful when systems observe the same economic event at slightly different times or when a known rounding convention creates small residuals.
Prefer the smallest tolerance justified by the business process:
1. identify the expected timing or rounding difference;
2. express the maximum accepted amount per asset;
3. keep evidence and alerting enabled above that amount;
4. review tolerances when settlement behavior changes.
A tolerance prevents a difference from opening an alert. It is not the same as [accepting an alert](/modules/reconciliation/alerts#accept-a-known-discrepancy), which records a human decision after evidence exists.
## Align sources with different timestamps
Multi-source templates support an independent point in time for every source. First evaluate the rule once and inspect `pitPerSource` to discover its stable keys, such as:
```json
{
"ledger:main#0": "2026-07-20T23:59:59Z",
"pool:01J...#0": "2026-07-20T23:30:00Z"
}
```
You can then replay or align the sources explicitly:
```json
{
"at": "2026-07-20T23:59:59Z",
"sourcePITs": {
"ledger:main#0": "2026-07-20T23:59:59Z",
"pool:01J...#0": "2026-07-20T23:30:00Z"
}
}
```
Values in `sourcePITs` are already effective timestamps, so the evaluation's safety margin is not subtracted from them. Unknown source keys and future timestamps are rejected.
## Choose the account sets carefully
A control is only as meaningful as its source selection. Before enabling a schedule:
- run the underlying Ledger and cash-pool queries independently;
- confirm that every intended account is included and no unrelated account matches;
- verify sign conventions with a known-good balance;
- test each configured asset;
- run a known failing scenario and inspect its evidence.
Ledger metadata queries at a historical point in time require Ledger 2.4.11 or later, which is the minimum supported by this Reconciliation release.
---
## Webhooks
Source: https://docs.formance.com/modules/webhooks
Webhooks converts events produced by Formance modules into outbound HTTP `POST` requests. Use it when your application must react to a Ledger transaction, a payment update, a workflow transition, or another Stack event without polling the source API.
A webhook is an asynchronous notification, not a remote procedure call. The source operation does not wait for your endpoint, and a successful source operation does not mean that your application has already processed its webhook.
## How Webhooks fits into the Stack
```mermaid
flowchart LR
Producer["Ledger, Payments,Orchestration, Reconciliation"] -->|"publishes an event"| Broker["Stack event broker"]
Broker -->|"matching event type"| Webhooks["Webhooks"]
Webhooks -->|"signed HTTP POST"| Endpoint["Your endpoint"]
Endpoint -->|"2xx or error"| Webhooks
```
Three terms describe this flow:
| Term | Meaning |
| --- | --- |
| **Event** | An immutable message published by a Formance module, such as `ledger.committed_transactions`. |
| **Delivery** | The work required to send one event to one webhook configuration. If three configurations subscribe to the same event, Webhooks creates three independent deliveries. |
| **Attempt** | One HTTP request for a delivery. A delivery can have several attempts when the endpoint times out or returns a retryable response. |
Webhooks 2.5.0 uses a durable delivery model. It stores matching deliveries before acknowledging the event broker, sends them through a separate dispatcher, records every HTTP attempt, and exposes delivery inspection and replay APIs. This separates event ingestion from endpoint availability: a slow or unavailable endpoint does not block Webhooks from accepting later events into its delivery queue.
See [Delivery lifecycle and guarantees](/modules/webhooks/deliveries) for the exact retry policy, state transitions, duplicate and ordering semantics, retention, and replay behavior.
## Create a webhook configuration
A configuration binds an endpoint to an explicit list of event types. Event type matching is exact and case-insensitive; Webhooks stores identifiers in lowercase.
{"fctl webhooks create \"https://example.com/webhooks/formance\" \"ledger.committed_transactions\" \"payments.saved_payment\""}
If you omit `secret`, Webhooks generates a 24-byte secret and returns it base64-encoded. Store it in your secrets manager: your endpoint needs it to verify every delivery.
Use an HTTPS endpoint in production. Event subscriptions do not currently support wildcards such as `ledger.*`; list every event type that the endpoint handles.
New configurations are active immediately. You can deactivate a configuration to stop new deliveries, reactivate it later, update its endpoint or event types, rotate its signing secret, send a test request, or delete it.
## What your endpoint receives
For every attempt, Webhooks sends:
- an HTTP `POST` request with `Content-Type: application/json`;
- a normalized Formance event envelope and its module-specific `payload`;
- a stable delivery identifier and, when provided by the source module, an idempotency key;
- an HMAC-SHA256 signature and a fresh attempt timestamp;
- a `formance-webhook-test` flag that distinguishes test requests from live events.
Webhooks waits up to 30 seconds for the endpoint response. A `2xx` response marks the delivery as successful.
Other responses are either retried or marked as permanently failed according to the [delivery policy](/modules/webhooks/deliveries#response-classification).
Retry classification and retry windows differ in earlier Webhooks releases. Upgrade to Webhooks 2.5.0 before relying on the bounded retry contract documented for the durable delivery model.
Return a `2xx` response only after your application has durably accepted the event. A common pattern is to verify the signature, reject stale requests, write the delivery to an internal queue or inbox table, and then respond. Perform slower business processing asynchronously.
## Build a reliable receiver
Your endpoint should implement four controls:
1. Verify the signature against the exact raw request body before parsing JSON.
2. Reject timestamps outside a short tolerance window to limit replay attacks.
3. Deduplicate deliveries by `formance-webhook-id` before applying business effects.
4. Process events without assuming that they arrive in source order.
The [Receiving webhooks](/modules/webhooks/receiving) guide documents the headers, signature input, verification code, idempotency strategy, secret rotation, and test deliveries.
## Explore Webhooks
Verify requests, prevent duplicate effects, and design a production endpoint.
Understand persistence, retries, delivery states, replay, retention, and operational limits.
Browse the Stack v3.2.8 event types, envelope, and payload shapes.
Configure the broker that carries events between Stack modules.
---
## Alerts and Evidence
Source: https://docs.formance.com/modules/reconciliation/alerts
An alert is the operational case created when one asset fails a reconciliation rule. It brings together the latest evidence, current status, alert severity, reconciliation period, and a complete transition history.
## Read evidence from a passing check
A passing check does not open an alert. Its evidence is stored on the evaluation returned by `POST /rules/{ruleID}/evaluate` and available later from the evaluation endpoints.
Each checked asset appears in `data.evidence` with `passed: true` and a compact, self-contained `proof`. It contains the observed balances and the predicate inputs needed to verify the historical result, even if the rule is edited later. The proof shape depends on the template:
`ledger_vs_pool_drift` stores the raw Ledger and cash-pool balances with the sign and tolerance used by the check. In this example, the residual is `100 × 1 + (-95) = 5`, which passes a tolerance of `5`.
```json
{
"fingerprint": "asset:USD/2",
"passed": true,
"proof": {
"ledger": "100",
"ledgerSign": "1",
"pool": "-95",
"tolerance": "5"
}
}
```
`ledger_invariant` stores the total positive and negative contributions after each term's configured sign is applied, together with the tolerance used to check their residual.
```json
{
"fingerprint": "asset:USD/2",
"passed": true,
"proof": {
"positive": "350",
"negative": "-350",
"tolerance": "0"
}
}
```
`account_threshold` stores the aggregate balance and its configured bounds. An unset `min` or `max` is omitted from the proof.
```json
{
"fingerprint": "asset:USD/2",
"passed": true,
"proof": {
"balance": "500",
"min": "400",
"max": "600"
}
}
```
`source_parity` stores both observed balances and the tolerance used to compare them. In this example, the absolute difference is `30`, which passes a tolerance of `30`.
```json
{
"fingerprint": "asset:USD/2",
"passed": true,
"proof": {
"left": "100",
"right": "130",
"tolerance": "30"
}
}
```
Proof amounts are integer strings in the asset's smallest unit; see [Unambiguous Monetary Notation](/modules/numscript/monetary-notation). Comparisons remain exact for arbitrarily large balances. PASS proofs omit generated expressions and derived fields, while retaining the inputs needed to verify the predicate; use `pitPerSource` for the source timestamps.
## Investigate an alert
Start with the alert's current state:
```json
{
"id": "",
"ruleID": "",
"fingerprint": "asset:USD/2",
"periodID": "2026-07",
"status": "OPEN",
"severity": "high",
"firstSeenAt": "2026-07-20T00:00:02Z",
"lastSeenAt": "2026-07-21T00:00:02Z",
"occurrenceCount": 2,
"lastEvaluationID": "",
"evidence": {
"asset": "USD/2",
"leftBalance": "250000",
"rightBalance": "249500",
"difference": "500",
"tolerance": 0
},
"labels": { "team": "treasury" }
}
```
Review four things before acting:
1. **Rule and period:** confirm which control and financial period the case belongs to.
2. **Evidence:** compare the observed values, difference, and tolerance.
3. **Source timestamps:** inspect the linked evaluation's `pitPerSource` to verify both sides were read at the intended instants.
4. **Timeline:** review earlier failures and manual decisions on the same case.
Alert lists are ordered by `lastSeenAt` descending, then by `id` descending when timestamps match. This stable order prevents alerts from moving across page boundaries while following a cursor.
Retrieve the append-only timeline with:
" }} noFctl />
The timeline is cursor-paginated and ordered most recent first. It includes `fail`, `pass`, `ack`, `resolve`, `accept`, `snooze`, and `unsnooze` events. A failure whose previous status was `RESOLVED` is marked as a reopen. If the alert does not exist, the endpoint returns `404` rather than an empty timeline.
### Build a chronological view
Fetch every page, following `cursor.next` while `cursor.hasMore` is true. Concatenate the returned `cursor.data` arrays, then reverse the complete list because the API returns newest events first:
```typescript
async function loadAlertTimeline(
stackUrl: string,
alertId: string,
token: string,
) {
const events = []
let next: string | undefined
do {
const url = new URL(
`/api/reconciliation/alerts/${alertId}/events`,
stackUrl,
)
url.searchParams.set("pageSize", "100")
if (next) url.searchParams.set("cursor", next)
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
})
if (!response.ok) throw new Error(`Timeline request failed: ${response.status}`)
const body = await response.json()
events.push(...body.cursor.data)
next = body.cursor.hasMore ? body.cursor.next : undefined
} while (next)
return events.reverse()
}
```
Each entry carries `prevStatus` and `newStatus`, so the timeline can show the state transition without inferring it from neighboring rows. Use `isReopen: true` to highlight a failure that followed a resolution.
| Event type | What to show |
| --- | --- |
| `fail` | The discrepancy evidence in `payload`; follow `evaluationID` for that evaluation's source timestamps. |
| `pass` | Automatic resolution; follow `evaluationID` for the passing proof and `pitPerSource`. |
| `ack` | The operator, timestamp, and note in `payload`. |
| `resolve` | The `fixed_by_booking` resolution, including its author, note, and transaction references. |
| `accept` | The `accepted_by_business` resolution and its frozen evidence snapshot. |
| `snooze` / `unsnooze` | When notification muting began, changed, or ended. |
Do not reconstruct history from the current alert row. Its `ack`, `resolution`, and `snooze` fields describe only the current state. The events endpoint is the source of truth for prior transitions and resolutions, including those cleared when an alert reopened.
## Acknowledge ownership
Acknowledge an alert when someone has started investigating it:
" }} noFctl
body={{ by: "analyst@example.com", note: "Checking provider settlement files" }} />
Acknowledgement changes `OPEN` to `ACKNOWLEDGED`. It does not make the period green and does not stop later evaluations. If another failure arrives, the alert returns to `OPEN` so the prior acknowledgement cannot hide new evidence.
## Snooze notifications
Use a snooze for a known, time-bounded situation such as a provider maintenance window or an in-flight migration:
" }} noFctl
body={{ by: "analyst@example.com", until: "2026-07-21T18:00:00Z", note: "Provider settlement replay in progress" }} />
A snooze mutes webhook notifications until `until`; it does not change the alert status. Evaluations continue, failures remain in the timeline, and the alert continues to count against the period's green status.
The first failure at or after the snooze expires clears it and notifies once. You can also lift it early:
" }} noFctl
body={{ by: "analyst@example.com" }} />
Reconciliation automatically suppresses webhook delivery for repeated failures whose evidence is materially unchanged. The event is still recorded and `occurrenceCount` still increases. Changed evidence, reopens, and manual transitions continue to notify.
## Record a corrective booking
After correcting the financial state, close the alert and optionally link the Ledger transactions that made the correction:
" }} noFctl
body={{
by: "analyst@example.com",
note: "Booked the missing provider fee",
transactionRefs: [""]
}} />
This records a `fixed_by_booking` resolution. A later failing evaluation within the same period reopens the same alert; the earlier resolution remains in the [event timeline](#build-a-chronological-view).
An alert also closes automatically when a later evaluation passes. Automatic closure is recorded as an `auto` resolution.
## Accept a known discrepancy
Sometimes the observed difference is valid but should remain visible—for example, a confirmed settlement timing difference at period close. Accept it with an explicit business decision:
" }} noFctl
body={{
by: "controller@example.com",
note: "Confirmed settlement lag; cash arrived in the next banking window"
}} />
Acceptance requires an author and non-empty note. Reconciliation freezes the alert's current evidence into the `accepted_by_business` resolution so later evidence cannot rewrite what was approved.
Acceptance closes the operational case; it does not change Ledger or external balances. Apply your organization's approval policy before treating an accepted alert as reconciled.
## Use webhook events
Reconciliation publishes alert transitions through the Formance event bus for delivery by the [Webhooks module](/modules/webhooks). Subscribe only to the transitions your workflow needs:
| Event | Meaning |
| --- | --- |
| `reconciliation.opened_alert` | A new asset and period failed. |
| `reconciliation.updated_alert` | An active discrepancy changed materially. |
| `reconciliation.acknowledged_alert` | An operator took ownership. |
| `reconciliation.resolved_alert` | The control passed or an operator recorded a fix. |
| `reconciliation.accepted_alert` | An authorized user accepted the discrepancy. |
| `reconciliation.reopened_alert` | A resolved case failed again in the same period. |
| `reconciliation.snoozed_alert` | Notifications were muted until a future time. |
| `reconciliation.unsnoozed_alert` | A mute was lifted early. |
Each payload contains the current alert and the event that caused the transition. Use the alert-event ID as the downstream idempotency key: webhook delivery can be retried and should not be treated as exactly once.
## Separate financial and operational failures
If a source read or evaluation cannot complete, Reconciliation stores an `ERROR` evaluation and opens an alert with fingerprint `engine.error` and label `kind=engine.error`. Route this signal to the platform team rather than treating it as a confirmed financial discrepancy.
Resolve the source or execution problem, then run the rule again. A successful evaluation closes the operational error path and produces the normal financial verdict.
---
## Receiving Webhooks
Source: https://docs.formance.com/modules/webhooks/receiving
A production webhook endpoint must authenticate the sender, preserve each delivery until it can be processed, and remain safe when the same delivery arrives more than once.
## Request contract
Webhooks sends an HTTP `POST` with a JSON body and the following headers:
| Header | Description |
| --- | --- |
| `formance-webhook-id` | Identifier for one delivery chain. It remains stable across automatic retries and, where replay is available, manual replay. |
| `formance-webhook-timestamp` | Unix timestamp generated for this HTTP attempt. Each retry receives a new timestamp. |
| `formance-webhook-signature` | One or more versioned signatures. The current format is `v1,`. |
| `formance-webhook-idempotency-key` | Source idempotency key, when the module that produced the event supplied one. Do not assume it is always present. |
| `formance-webhook-test` | `true` for a request sent by the configuration test endpoint, otherwise `false`. |
| `Content-Type` | `application/json`. |
Webhooks signs the raw request body, including whitespace and field ordering.
See the [event reference](/modules/webhooks/events) for the Stack v3.2 envelope and payload catalog.
## Verify the signature
For signature version `v1`, Webhooks computes HMAC-SHA256 over:
```text
{formance-webhook-id}.{formance-webhook-timestamp}.{raw-request-body}
```
The result is base64-encoded and sent as `v1,`. The verification helper compares signatures in constant time, but it does not enforce timestamp freshness. Apply your own tolerance after parsing the timestamp.
```go verify-webhook.go
package example
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/formancehq/webhooks/pkg/security"
)
const signatureTolerance = 5 * time.Minute
func VerifyWebhook(r *http.Request, secret string) ([]byte, error) {
id := r.Header.Get("formance-webhook-id")
timestampHeader := r.Header.Get("formance-webhook-timestamp")
signatures := r.Header.Get("formance-webhook-signature")
if id == "" || timestampHeader == "" || signatures == "" {
return nil, fmt.Errorf("missing webhook signature headers")
}
timestamp, err := strconv.ParseInt(timestampHeader, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid webhook timestamp: %w", err)
}
attemptTime := time.Unix(timestamp, 0)
if age := time.Since(attemptTime); age < -signatureTolerance || age > signatureTolerance {
return nil, fmt.Errorf("webhook timestamp outside tolerance")
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("read webhook body: %w", err)
}
verified, err := security.Verify(signatures, id, timestamp, secret, body)
if err != nil {
return nil, fmt.Errorf("verify webhook signature: %w", err)
}
if !verified {
return nil, fmt.Errorf("invalid webhook signature")
}
return body, nil
}
```
Do not parse and re-serialize the JSON before verification. Any byte-level change produces a different signature. Keep your server clock synchronized so a legitimate request is not rejected by the timestamp check.
## Acknowledge after durable acceptance
Webhooks treats every `2xx` response as success. Return success after the event is stored in a system that survives process failure, such as a database-backed inbox or durable queue. If you respond before storing the event and then crash, Webhooks has no reason to retry it.
A typical receiver follows this sequence:
```mermaid
sequenceDiagram
participant W as Webhooks
participant E as Endpoint
participant I as Inbox table or queue
participant P as Business processor
W->>E: Signed POST
E->>E: Verify signature and timestamp
E->>I: INSERT delivery ID and raw body
I-->>E: Committed
E-->>W: 204 No Content
I->>P: Process asynchronously
```
If validation or durable acceptance fails, return a non-`2xx` status that reflects whether another attempt could succeed.
Webhooks retries `408`, `429`, `5xx`, transport errors, and timeouts. Other `4xx` responses are terminal.
Earlier releases use a different retry policy. Do not use a `4xx` response to control retries without checking the behavior of your selected Webhooks version.
## Make processing idempotent
Webhook delivery can be duplicated at the HTTP boundary. A worker can lose its database connection after your endpoint accepted a request but before Webhooks recorded the successful attempt. Recovery can then send the event again.
Create a unique constraint on `formance-webhook-id`, then record the identifier and the business result in one transaction. If the identifier already exists, return the same successful response without applying the effect again. The delivery ID remains stable across automatic retries.
Manual replay also preserves the delivery ID.
Also make the business operation idempotent from stable domain identifiers in the payload when possible. Use `formance-webhook-idempotency-key` as additional context when present, but do not rely on it exclusively because producers can leave it empty.
## Do not depend on delivery order
Webhooks dispatches several deliveries concurrently. A retry can also allow a later event to arrive before an earlier one. If order matters, use the event's resource identifiers and timestamps to reconcile state, or fetch the current resource from its source API before applying a transition.
## Test an endpoint
Use the configuration test operation to exercise network access, signature verification, and response handling:
Test requests send `{"data":"test"}`, set `formance-webhook-test: true`, and are not retried or stored as durable deliveries. They test network access, signing, and the immediate endpoint response; they do not prove that a specific module event subscription or production payload handler is correct. Validate live event handling in a non-production Stack before enabling business effects.
## Rotate a secret
Secret rotation takes effect immediately; Webhooks does not provide a sender-managed dual-secret grace period. Deploy the new secret to your receiver in coordination with the rotation.
The dispatcher reads the current configuration before each attempt, so queued attempts use the new secret after rotation. A request that was already in flight can still carry the previous signature. During a controlled transition, let your receiver temporarily accept both secrets, rotate the Webhooks configuration, then remove the previous secret after in-flight requests have completed.
---
## Delivery Lifecycle and Guarantees
Source: https://docs.formance.com/modules/webhooks/deliveries
Webhooks 2.5.0 uses one durable delivery model that separates broker ingestion from outbound HTTP attempts. This page defines what the pipeline guarantees, where duplicates can occur, how retries are scheduled, and how operators inspect and replay deliveries.
The worker does not expose a runtime pipeline selector. Delivery inspection and replay APIs use the same persisted model as automatic dispatch.
## Upgrade from the attempts model
New installations start directly with durable deliveries. Existing installations that still have outstanding retries in the former `attempts` table require a coordinated upgrade:
1. Stop every old Webhooks worker and wait for it to terminate.
2. Apply the new database migrations.
3. Run `webhooks backfill-deliveries` until it completes.
4. Deploy the new Webhooks version and recreate its workers.
The backfill command is resumable and idempotent. It exists only as an upgrade adapter: the runtime does not read or write the old attempts queue, and running old and new workers together is not supported. Operator-managed installations coordinate this sequence during the upgrade.
## Delivery pipeline
```mermaid
flowchart LR
Broker["Event broker"] --> Consumer["Webhooks consumer"]
Consumer -->|"transactional insert"| Deliveries["deliveries table"]
Deliveries -->|"commit succeeds"| Ack["Broker ACK"]
Deliveries --> Dispatcher["Concurrent dispatcher"]
Dispatcher --> Endpoint["Configured endpoint"]
Endpoint --> Attempt["Append-only attempt record"]
Attempt --> Deliveries
```
For each broker event, Webhooks selects active configurations that subscribe to its exact event type. In one database transaction, it creates one `pending` delivery per matching configuration. The pair `(event ID, configuration ID)` is unique, so broker redelivery cannot enqueue the same delivery twice. Webhooks acknowledges the broker message only after this transaction commits.
The dispatcher then claims due rows with database locks, marks them `delivering`, and performs outbound requests independently of broker consumption. Multiple workers can claim work without intentionally selecting the same row.
## Durable guarantee summary
| Property | Contract |
| --- | --- |
| **Acceptance** | A matching delivery is stored before the broker event is acknowledged. If persistence fails, the broker can redeliver the event. |
| **Broker deduplication** | Repeated broker delivery of the same event does not create another delivery for the same configuration. |
| **HTTP delivery** | At-least-once attempts until the endpoint returns `2xx` or the delivery reaches a terminal condition. Successful processing by the receiver is not guaranteed. |
| **Duplicates** | Possible. A request can reach the endpoint before Webhooks loses the response or fails to commit the attempt result. Receivers must deduplicate. |
| **Ordering** | Not guaranteed across events or configurations. The dispatcher is concurrent and retries can overtake earlier deliveries. |
| **Timeout** | Each HTTP attempt has a 30-second timeout. |
| **Retry budget** | At most 15 attempts and at most 10 hours per retry generation with default settings. The first limit reached terminates the generation. |
| **Manual recovery** | Failed deliveries can receive a fresh retry generation through the replay API. Pending deliveries can be expedited. |
This is not an exactly-once protocol. Exactly-once business effects require idempotency in the receiving application.
## Durable delivery states
| Status | Meaning |
| --- | --- |
| `pending` | Stored and waiting for its first or next attempt. `nextAttemptAt` indicates when it becomes eligible. |
| `delivering` | Claimed by a dispatcher worker and currently in flight. |
| `succeeded` | The endpoint returned a `2xx` response. This is terminal. |
| `failed` | The endpoint returned a permanent error, or the retry count or elapsed retry window was exhausted. |
| `cancelled` | Delivery stopped because its configuration was deactivated or deleted. |
Every HTTP call creates an append-only attempt record with the endpoint, attempt number, replay generation, outcome, status code, duration, response excerpt, error, and timestamp. If a worker stops while a delivery is `delivering`, claims older than five minutes are recovered: active configurations return to `pending`; inactive or deleted configurations become `cancelled`.
## Response classification
Webhooks classifies the final response from the endpoint as follows:
| Result | Delivery action |
| --- | --- |
| `2xx` | Mark `succeeded`; no more automatic attempts. |
| `408 Request Timeout` | Retry. |
| `429 Too Many Requests` | Retry and honor a valid `Retry-After` when it requests a longer delay. |
| Other `4xx` | Mark `failed` immediately. These errors normally require a configuration or application change. |
| `5xx` | Retry. A valid `Retry-After` can extend the delay. |
| Network error or 30-second timeout | Retry. The attempt records status code `0`. |
The response body does not affect classification. Webhooks reads at most 64 KiB of it for diagnostics.
Return `429` with `Retry-After` when your service is healthy but temporarily rate-limited. Return another `4xx` only when retrying the same request cannot succeed without a change, because Webhooks treats it as terminal.
## Retry schedule
The default policy uses exponential backoff without jitter. The first request is immediate, then delays double from one minute until they reach the one-hour cap:
| Attempt | Approximate time from first attempt |
| --- | --- |
| 1 | Immediately |
| 2 | 1 minute |
| 3 | 3 minutes |
| 4 | 7 minutes |
| 5 | 15 minutes |
| 6 | 31 minutes |
| 7 | 1 hour 3 minutes |
| 8 | 2 hours 3 minutes |
| 9–15 | Once per hour, with attempt 15 at approximately 9 hours 3 minutes |
Automatic retries stop when either limit is reached:
- 15 total attempts in the current generation;
- 10 elapsed hours from the first attempt in the current generation.
For retryable responses, Webhooks uses `Retry-After` when it is valid and longer than the computed backoff. It accepts delay-seconds or an HTTP date, caps an endpoint-supplied delay at six hours, and never allows it to extend the 10-hour retry window.
Webhooks does not open a per-endpoint circuit breaker or automatically disable a configuration after repeated failures. Attempt and time caps bound each delivery; operators must monitor terminal failures and queue growth.
## Configuration changes during delivery
Deactivating or deleting a configuration cancels its pending deliveries. An in-flight attempt can finish, but any non-successful result becomes `cancelled` instead of returning to the queue. Reactivating a configuration does not resurrect cancelled deliveries; replay also rejects cancelled deliveries.
Updating an endpoint affects later attempts because the dispatcher reads the current configuration before sending. Rotating the secret similarly causes later attempts to use the new secret.
## Inspect deliveries
List deliveries by configuration, status, or creation window. The list excludes payloads and uses an opaque cursor; request one delivery to retrieve its payload.
```sh
curl --get "$STACK_URL/api/webhooks/deliveries" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--data-urlencode "configId=$CONFIG_ID" \
--data-urlencode "status=failed" \
--data-urlencode "createdAtFrom=2026-08-01T00:00:00Z" \
--data-urlencode "pageSize=100"
```
Valid statuses are `pending`, `delivering`, `succeeded`, `failed`, and `cancelled`. `pageSize` defaults to 100 and cannot exceed 1,000.
Retrieve a delivery and its attempt history:
```sh
curl "$STACK_URL/api/webhooks/deliveries/$DELIVERY_ID" \
--header "Authorization: Bearer $ACCESS_TOKEN"
curl "$STACK_URL/api/webhooks/deliveries/$DELIVERY_ID/attempts?pageSize=100" \
--header "Authorization: Bearer $ACCESS_TOKEN"
```
Use `lastStatusCode`, `lastError`, `nextAttemptAt`, and the attempts list to distinguish an endpoint rejection from a timeout, retry backlog, or exhausted retry budget.
## Replay one delivery
Only `failed` and `pending` deliveries belonging to an active configuration are eligible for replay. A replay request requires an `Idempotency-Key` header.
```sh
curl --request POST "$STACK_URL/api/webhooks/deliveries/$DELIVERY_ID/replay" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Idempotency-Key: replay-$DELIVERY_ID-20260805"
```
For a `failed` delivery, replay increments `replayGeneration`, resets its attempt count and 10-hour retry window, and queues it immediately. For a `pending` delivery, replay only moves the next attempt to now; it does not reset the current budget. `succeeded`, `delivering`, and `cancelled` deliveries are not eligible for replay.
The same idempotency key and request return the original replay result for 24 hours. Reusing the key for a different replay returns a conflict.
## Replay a bounded set
Bulk replay operates synchronously on one page of at most 1,000 deliveries. A request must include `createdAtFrom`; its creation-time window must be positive and cannot exceed 90 days.
```sh
curl --request POST "$STACK_URL/api/webhooks/deliveries/replay" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: incident-20260805-page-1" \
--data '{
"createdAtFrom": "2026-08-05T08:00:00Z",
"createdAtTo": "2026-08-05T10:00:00Z",
"statuses": ["failed", "pending"],
"configIds": ["'"$CONFIG_ID"'"],
"pageSize": 1000
}'
```
The response separates `replayed` failed deliveries, `expedited` pending deliveries, and `skipped` rows. When `hasMore` is true, submit another request with `nextCursor` and a new `Idempotency-Key`. Keep the original filters unchanged; the cursor binds to the time window, statuses, and configuration IDs.
## Retention and observability
With default settings, Webhooks retains succeeded deliveries for 30 days and failed or cancelled deliveries for 90 days. Attempt history is deleted with its delivery. Retention runs hourly. These values are runtime settings for self-hosted deployments and can be changed or disabled.
Webhooks exports OpenTelemetry traces and the following delivery metrics when metrics export is configured:
| Metric | Purpose |
| --- | --- |
| `webhooks_delivery_attempts_total` | Attempts by outcome and HTTP status class. |
| `webhooks_delivery_duration_seconds` | Outbound request duration. |
| `webhooks_retry_queue_depth` | Pending delivery count, capped at 1,000,000. |
| `webhooks_replayed_deliveries_total` | Deliveries replayed or expedited manually. |
| `webhooks_delivery_transitions_total` | Durable delivery state transitions. |
| `webhooks_delivery_claims_recovered_total` | Stale in-flight claims recovered after worker interruption. |
Alert on a growing retry queue, sustained `5xx` or timeout rates, terminal failures, and old `pending` deliveries. A delivery can be safely considered complete only when it is `succeeded`, or when your operational process has accepted its terminal failure.
---
## Event Reference
Source: https://docs.formance.com/modules/webhooks/events
This reference lists the webhook events and JSON payload formats published with [Formance Stack v3.2.8](https://github.com/formancehq/stack/releases/download/v3.2.8/all.json). It uses Ledger events v2.0.0, Orchestration events v2.0.0, Payments events v3.0.0, and Reconciliation events v2.4.0 from that release artifact.
## Event envelope
Every delivery uses the same top-level envelope. Webhooks builds the subscription identifier by lowercasing the producer message's `app` and `type` fields and joining them with a dot. It also writes that normalized identifier back to the outgoing `type` field. For example, `app: ledger` and producer type `COMMITTED_TRANSACTIONS` become `type: ledger.committed_transactions` in the webhook body.
The `payload` object changes with the event type. This example shows a complete Ledger committed-transactions message:
```json
{
"idempotency_key": "order-2026-0042",
"app": "ledger",
"version": "v2",
"date": "2026-08-05T10:30:00Z",
"type": "ledger.committed_transactions",
"payload": {
"ledger": "main",
"transactions": [
{
"postings": [
{
"source": "world",
"destination": "users:001",
"amount": 1000,
"asset": "USD/2"
}
],
"metadata": {
"order": "ORD-2026-0042"
},
"id": 42,
"timestamp": "2026-08-05T10:30:00Z",
"reverted": false
}
]
}
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency_key` | string | Yes | Idempotency key supplied by the producing module. The field is present but can be an empty string. |
| `version` | string | Yes | Version of the module event contract. |
| `date` | string (`date-time`) | Yes | Time at which the event was published. |
| `app` | string | Yes | Module that published the event. |
| `type` | string | Yes | Normalized lowercase `.` identifier shown below. |
| `payload` | object | Yes | Event-specific data. |
The Stack release artifact describes producer envelopes and event payloads. Webhooks normalizes the outgoing envelope as shown above, while the nested `payload` remains governed by the event's JSON Schema. The tables below summarize its top-level shape. Use the linked v3.2.8 artifact for required payload fields, nested objects, array items, formats, and enum values when generating validators or types.
## Ledger events
| Webhook event type | Payload shape |
| --- | --- |
| `ledger.committed_transactions` | `ledger`: string; `transactions`: array of objects |
| `ledger.deleted_metadata` | `ledger`: string; `targetType`: string; `targetId`: string; `key`: string |
| `ledger.reverted_transaction` | `ledger`: string; `revertedTransaction`: object; `revertTransaction`: object |
| `ledger.saved_metadata` | `ledger`: string; `targetType`: string; `targetId`: string; `metadata`: object |
## Orchestration events
| Webhook event type | Payload shape |
| --- | --- |
| `orchestration.failed_trigger` | `id`: string; `triggerID`: string; `error`: string |
| `orchestration.failed_workflow` | `id`: string; `instanceID`: string; `error`: string |
| `orchestration.failed_workflow_stage` | `id`: string; `instanceID`: string; `number`: integer; `error`: string |
| `orchestration.started_workflow` | `id`: string; `instanceID`: string |
| `orchestration.started_workflow_stage` | `id`: string; `instanceID`: string; `number`: integer |
| `orchestration.succeeded_trigger` | `id`: string; `triggerID`: string |
| `orchestration.succeeded_workflow` | `id`: string; `instanceID`: string |
| `orchestration.succeeded_workflow_stage` | `id`: string; `instanceID`: string; `number`: integer |
## Payments events
| Webhook event type | Payload shape |
| --- | --- |
| `payments.connector_reset` | `createdAt`: string; `connectorID`: string |
| `payments.deleted_pool` | `id`: string; `createdAt`: string |
| `payments.saved_account` | `id`: string; `provider`: string; `connectorID`: string; `createdAt`: string; `reference`: string; `type`: string; `rawData`: object; `defaultAsset`: string; `name`: string; `metadata`: object |
| `payments.saved_balance` | `accountID`: string; `connectorID`: string; `provider`: string; `createdAt`: string; `lastUpdatedAt`: string; `asset`: string; `balance`: number |
| `payments.saved_bank_account` | `id`: string; `createdAt`: string; `name`: string; `accountNumber`: string; `iban`: string; `swiftBicCode`: string; `country`: string; `metadata`: object; `relatedAccounts`: array |
| `payments.saved_payment` | `id`: string; `connectorID`: string; `provider`: string; `reference`: string; `createdAt`: string; `type`: string; `status`: string; `scheme`: string; `asset`: string; `amount`: number; `initialAmount`: number; account IDs, links, metadata, and raw data |
| `payments.saved_payment_initiation` | `id`: string; `connectorID`: string; `provider`: string; `reference`: string; `createdAt`: string; `scheduledAt`: string; `description`: string; `type`: string; `amount`: number; `asset`: string; account IDs and metadata |
| `payments.saved_payment_initiation_adjustment` | `id`: string; `paymentInitiationID`: string; `status`: string; `amount`: number; `asset`: string; `error`: string; `metadata`: object |
| `payments.saved_payment_initiation_related_payment` | `paymentInitiationID`: string; `paymentID`: string |
| `payments.saved_pool` | `id`: string; `name`: string; `createdAt`: string; `accountIDs`: array of strings |
## Reconciliation events
| Webhook event type | Payload shape |
| --- | --- |
| `reconciliation.accepted_alert` | `alert`: object; `event`: object |
| `reconciliation.acknowledged_alert` | `alert`: object; `event`: object |
| `reconciliation.opened_alert` | `alert`: object; `event`: object |
| `reconciliation.reopened_alert` | `alert`: object; `event`: object |
| `reconciliation.resolved_alert` | `alert`: object; `event`: object |
| `reconciliation.snoozed_alert` | `alert`: object; `event`: object |
| `reconciliation.unsnoozed_alert` | `alert`: object; `event`: object |
| `reconciliation.updated_alert` | `alert`: object; `event`: object |
The `alert` object contains the alert state and evidence. The `event` object records the transition, including its previous and new status, actor metadata, notification decision, and timestamps. Refer to the release artifact for the complete nested schemas and enum values.
---
## Architecture
Source: https://docs.formance.com/manage/architecture
The Formance Platform embraces a cloud native architecture, enabling seamless deployments in various environments such as Formance Cloud, on-prem, or hybrid approaches.
Formance Cloud offers minimal effort deployments with sane defaults and rolled-in support. Alternatively, on-prem and hybrid options provide a great level of flexibility — which has proven to be beneficial for deployments existing within the infrastructure of regulated financial institutions and platforms.
## Planes
The Formance Platform is composed of two main planes: the control plane, and the data plane. The control plane is responsible for managing the platform and provisioning its components, while the data plane is responsible for delivering the actual core functions of the platform.
**Trivia:** One non-intuitive thing to note is: _data_ in the control plane / data plane terminology refers not only to data storage, but to the storage and compute of services providing core functionality of the platform.
## Control plane
The control plane server is responsible for managing the platform and its components, while the control plane client is responsible for provisioning the data plane according to the desired state, and for facilitating communication between the data plane and the control plane server.
### Components
## Data plane
The data plane is composed of a number of components that work together to provide the core functionality of the platform. These components are either considered core services that deliver a specific piece of functionality to the platform (identified below as group "product"), or support services that provide horizontal support to the platform (identified below as group "system").
### Components
---
## Single sign-on (SSO) for organizations
Source: https://docs.formance.com/manage/identity/sso
You can enable SSO on a per-organization basis using your existing identity provider. Supported providers are OIDC, Microsoft Entra ID (formerly Azure AD), Google, and GitHub.
## Prerequisites
- A Formance Cloud organization and access to `fctl`
- A client application created in your identity provider (Client ID and Client Secret)
When creating the app in your IdP, set the redirect URI to your membership endpoint callback: `/api/authorize/callback`. See step 1 to determine your membership URI.
## Determine your membership URI
Use the same membership URI you pass to `fctl login`.
```bash
fctl login --membership-uri https://membership.BASE_URL/api
```
The SSO redirect URI to register in your IdP is therefore:
```text
https://membership.BASE_URL/api/authorize/callback
```
After you create the SSO configuration, the exact redirect URI is also displayed in the UI.
## Configure the authentication provider
The command format is:
```text
fctl cloud organizations authentication-provider configure \
[--oidc-issuer ] [--microsoft-tenant ]
```
- **type**: one of `oidc`, `microsoft`, `github`, `google`
- **name**: human-friendly provider name shown to users
- **client-id / client-secret**: values from your IdP app
- **--oidc-issuer**: required for `oidc` (e.g., `https://accounts.example.com`)
- **--microsoft-tenant**: required for `microsoft` (tenant ID or verified domain)
```bash
fctl cloud organizations authentication-provider configure \
oidc "My OIDC" "" "" \
--oidc-issuer https://accounts.example.com
```
```bash
fctl cloud organizations authentication-provider configure \
microsoft "My Entra" "" "" \
--microsoft-tenant ""
```
```bash
fctl cloud organizations authentication-provider configure \
google "Google" "" ""
```
```bash
fctl cloud organizations authentication-provider configure \
github "GitHub" "" ""
```
### Microsoft Entra ID
To configure Microsoft Entra ID as your identity provider, use the `oidc` type with the Microsoft issuer URL.
The `--oidc-issuer` must follow this format:
```text
https://login.microsoftonline.com//v2.0
```
Replace `` with your Entra tenant ID.
```bash
fctl cloud organizations authentication-provider configure \
oidc "msentra" "" "" \
--oidc-issuer "https://login.microsoftonline.com//v2.0"
```
Users must have an email address configured on their Entra identity to sign in via SSO.
Ensure the redirect URI in your IdP exactly matches `/api/authorize/callback`. Mismatches (scheme, host, path, or trailing slash) will cause sign-in failures.
## Verify SSO
Use the email domain associated with your IdP, if auto-login by domain is enabled.
From the Formance Cloud portal, choose the newly configured provider and complete the sign-in flow.
You should land back in the portal authenticated to your organization. If not, confirm the redirect URI and client credentials in your IdP and re-run the configure command if needed.
## Reference
```bash
fctl cloud organizations authentication-provider configure -h
```
```text
Configure the authorization provider for the organization
Usage:
fctl cloud organizations authentication-provider configure [flags]
Flags:
-h, --help help for configure
--microsoft-tenant string Microsoft tenant ID (used when type is 'microsoft') (default "tenant")
--oidc-issuer string OIDC issuer URL (used when type is 'oidc')
Global Flags:
-c, --config string Path to configuration file
-d, --debug Enable debug mode
--insecure-tls Allow insecure TLS connections
--organization string Selected organization (not required if only one organization is present)
-o, --output string Output format (plain, json)
-p, --profile string Configuration profile to use
--stack string Specific stack (not required if only one stack is present)
--telemetry Enable telemetry
```
---
## Access Control
Source: https://docs.formance.com/manage/identity/rbac
---
## Invite users
Source: https://docs.formance.com/manage/identity/inviting-team
---
## Audit Logs
Source: https://docs.formance.com/manage/identity/audit-logs
The Formance Platform ships with an Audit Log feature that streams every administrative action and every API request against your stacks to a destination of your choice — typically a SIEM such as Splunk, Datadog, or Elastic, or any HTTPS endpoint that accepts JSON POSTs. This page describes what's captured, how forwarding works, and what the events look like on the wire.
To enable Audit Log forwarding, contact your account team.
## What's captured
Two independent streams are available, and you can subscribe to either or both.
### Lifecycle events
Administrative activity at the organization and stack level: invitations, user and permission changes, stack create / update / delete / upgrade, region changes, module enable / disable, and similar.
Volume is proportional to administrative activity, not to your API traffic, so it tends to be light.
The following lifecycle event types are emitted:
### Module HTTP audit
Every non-streaming API call against your module services (ledger, payments, wallets, reconciliation, orchestration, and so on). Each event captures:
Volume scales one-to-one with your API call rate.
## How forwarding works
The Formance Platform pushes events to an HTTPS endpoint you provide. There is no queue for you to subscribe to and no agent to deploy — events arrive at your endpoint as POST requests as they happen, with the headers you've configured.
To set up forwarding, share with your account team:
- The HTTPS URL where events should be delivered.
- Any authentication headers to include on the POST. For Splunk HEC this is typically `Authorization: Splunk `.
- Which stream(s) you want — lifecycle, module, or both.
- The stacks the integration should cover, if you have multiple.
- Whether you want events batched. By default each event is delivered as its own POST; we can batch into bulk POSTs on request.
Provisioning is operated by Formance.
## Event format
Both streams share a common envelope:
```json
{
"date": "",
"app": "gateway | membership",
"version": "v2",
"type": "",
"payload": { ... }
}
```
The shape of `payload` depends on the stream and event type.
### Module audit event
```json
{
"date": "2026-04-30T09:23:41.123Z",
"app": "gateway",
"version": "v2",
"type": "AUDIT",
"payload": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"actor": {
"claims": { "sub": "...", "email": "...", "iss": "..." },
"organization_id": "...",
"stack_id": "...",
"ip_address": "..."
},
"http": {
"request": {
"method": "POST",
"path": "/api/ledger/v2/main/transactions",
"host": "...",
"header": { "Content-Type": ["application/json"] },
"body": "..."
},
"response": {
"status_code": 200,
"headers": { "Content-Type": ["application/json"] },
"body": "..."
}
}
}
}
```
When the request's JWT can't be validated, `actor.claims` is omitted and `actor.token_validation_error` carries the reason instead.
### Lifecycle event
Example — a stack creation:
```json
{
"date": "2026-04-30T09:15:00Z",
"app": "membership",
"version": "v2",
"type": "stacks.created",
"payload": {
"ownerId": "...",
"organizationId": "...",
"subject": "...",
"data": {
"id": "...",
"name": "prod-stack",
"regionId": "..."
}
}
}
```
The structure of `payload.data` varies by event type. The full list of lifecycle payload shapes is available on request from your account team.
## What's stripped before delivery
Module audit events are intentionally complete. Auditors and security teams need to be able to reconstruct exactly what happened, so headers and bodies are captured verbatim — with three exceptions, applied at the source:
- The `Authorization` request header is removed.
- The response body for `/api/auth/oauth/token` is cleared.
- Request and response bodies for streaming endpoints (Content-Type `application/vnd.formance*-stream` or `application/octet-stream`) are not captured.
Everything else is preserved as it appeared on the wire.
Module audit events will contain whatever your API calls contain. For ledger that means transaction amounts, account references, and any metadata you've attached. For payments, counterparty details and amounts. Once events are delivered to your SIEM, they live under your access controls, retention rules, and downstream pipelines — treat the audit stream as an extension of the data plane it observes, and apply the same data governance you'd apply to any system handling financial or personal information.
## Delivery semantics
Events are delivered at least once. If your endpoint returns a non-2xx response or is unreachable, the Formance Platform retries. During extended outages on your side, events may be queued internally up to a bounded retention window; sustained outages can cause queued events to be lost. Treat the destination receiver as durable storage rather than relying on the Formance Platform as a long-term buffer.
There is no API to replay historical events from before forwarding was set up. If you need historical data, request it during onboarding.
## Inspection from the command line
For ad-hoc inspection of lifecycle events, `fctl` provides direct access:
```sh
fctl cloud org history ""
fctl stack history ""
```
This is intended for occasional review rather than continuous ingestion. For SIEM integration, use forwarding as described above.
## FAQ
**Which destinations are supported?** Any HTTPS endpoint that accepts JSON POSTs. Splunk HEC, Datadog, Elastic, and custom endpoints are all in production with existing customers.
**Can we filter events at the source?** Not today — the configured stream is forwarded in full. Filter at your destination.
**Are the two streams delivered on the same connection?** They share an envelope but come from independent forwarders. Distinguish on the `app` field (`gateway` for module audit, `membership` for lifecycle) and on `type`.
**Is the schema stable?** Yes. The current schema version is `v2`. Changes will be coordinated with your account team.
**What's the latency from event to delivery?** Module audit events are forwarded as the request completes. Lifecycle events are forwarded as they occur. End-to-end latency to your endpoint is typically sub-second under normal conditions.
---
## Event Streaming
Source: https://docs.formance.com/manage/events/streaming
The Formance Platform can stream all platform events to a message broker, giving you a real-time feed of everything happening across your stack. This enables you to build reactive integrations, power audit pipelines, or feed events into your own data infrastructure.
## Overview
When a broker is configured, every service in the stack publishes events to it. Events are organized into topics in the format:
```
{stackName}-{module}
```
The following services produce events:
| Role | Services |
|------|----------|
| Producers | Ledger, Gateway, Payments |
| Consumers | Orchestration, Webhooks |
Webhooks are built on top of the event stream — they subscribe to broker topics and forward matching events to your configured endpoints. See [Webhooks](/modules/webhooks) for more details.
## Supported brokers
The platform supports two message brokers:
| Broker | Requirements |
|--------|-------------|
| [NATS](https://nats.io/) | Version 2.6+ with Jetstream enabled |
| [Kafka](https://kafka.apache.org/) | Standard Kafka cluster |
## Configuration
The broker is configured at the stack level via the `broker.dsn` setting.
```yaml
apiVersion: formance.com/v1beta1
kind: Settings
metadata:
name: formance-dev-broker
spec:
key: broker.dsn
stacks:
- "formance-dev"
value: nats://nats.formance-system.svc:4222?replicas=3
```
```yaml
apiVersion: formance.com/v1beta1
kind: Settings
metadata:
name: formance-dev-broker
spec:
key: broker.dsn
stacks:
- "formance-dev"
value: kafka://kafka.formance-system.svc:9092
```
For detailed broker setup instructions, see [Message Broker infrastructure](/deploy/self-hosted/infrastructure/message-broker).
---
## SDKs
Source: https://docs.formance.com/manage/sdks
The Formance Platform comes with a set of client libraries that you can use to connect your app, while ensuring you are always using the API in a consistent and up-to-date manner.
## Supported platforms
---
## Release Policy
Source: https://docs.formance.com/release-policy
This page describes the release policy for the Formance Platform and its components. This policy is subject to change; the latest version is the one available on this website.
The Formance Platform is released as a Stack: a coherent, reproducible, and supported set of components.
## Stack releases
A Stack release is a versioned compatibility line backed by an exact manifest of component versions.
The Stack version exposed at runtime identifies the compatibility line, for example `v3.2`. The exact composition of the Stack is pinned in the Helm Chart and versioned through chart revisions. A deployment never resolves component versions dynamically: the installed component versions are the versions pinned by the applied chart.
The Stack version keeps three segments, but it does not follow strict SemVer:
| Segment | Meaning |
| --- | --- |
| Major | Breaks the Stack compatibility contract |
| Minor | Introduces a new Stack compatibility line |
| Patch | Publishes a new compatible chart and manifest revision in the same Stack line |
For example, `v3.2.x` represents the `v3.2` compatibility line. A new chart revision for that line can update one or more compatible components while the runtime Stack line remains `v3.2`.
## Compatibility contract
All components embedded in the same Stack line must be compatible with:
- the Ledger version embedded by that line;
- the other embedded components;
- the public APIs exposed by the Stack line;
- the migrations and schemas supported by the Stack line;
- the operational constraints of the target deployment.
A component version can only be integrated into an existing Stack line if it respects that contract. If it requires an incompatible API change, migration, Ledger dependency, or operational requirement, it must be released through a new Stack compatibility line or documented as an explicit exception.
## Component release regimes
Components keep their own SemVer versioning. Within a component major version, releases are expected to remain compatible with that component's public contract.
The Stack does not mechanically propagate component minor or patch versions into the Stack version. For components other than Ledger, compatible minor and patch bumps can be integrated into an existing Stack line as Stack patch revisions. A new Stack compatibility line is required when one of those components moves to a new major version, or otherwise introduces a change that breaks the Stack compatibility contract.
Whether that new Stack line is a Stack minor or a Stack major depends on the impact on the Stack contract: a Stack minor introduces a new compatibility line, while a Stack major breaks compatibility for existing users.
### Ledger
Ledger is pinned for each supported Stack line.
Ledger holds client data and carries high-impact migrations, so each Stack line pins an exact Ledger version. Fixes can be backported to the supported Ledger line, then integrated by publishing a new compatible chart revision. An incompatible Ledger migration requires a new Stack compatibility line.
### Rolling within major components
The following components are released as rolling within major components:
- `transaction-plane`
- `payments`
- `wallets`
- `flows`
- `reconciliation`
- `webhooks`
- `auth`
- `gateway`
These components keep their own SemVer, but compatible features and fixes can be integrated into an existing Stack line as long as they remain compatible with the component major version and the Stack compatibility contract.
A compatible bump of one of these components produces a Stack patch revision. This means a Stack patch can include a new feature from a rolling component, provided the feature does not break the Stack line.
## Helm Chart and Versions CRD
The Helm Chart is the technical source of truth for the exact composition of a Stack release. For a published chart revision, the manifest is immutable. Any component version change requires a new chart revision and a change visible in Git.
The `Versions` CRD (`versions.formance.com`) exposes the installed Stack compatibility line and the component versions embedded by the applied chart. It must make support and diagnostics able to map a Stack line to concrete component versions, for example:
```yaml
stack: v3.2
modules:
ledger: 2.3.4
transaction-plane: 1.4.2
payments: 1.12.3
wallets: 1.7.0
flows: 1.9.1
reconciliation: 1.3.5
webhooks: 1.5.0
auth: 1.8.2
gateway: 1.6.4
```
## Changelog
The Stack changelog is part of the release contract. Each Stack release must list the component deltas since the previous chart revision in the same line, including:
- features;
- fixes;
- security fixes;
- migrations;
- configuration changes;
- known risks or required operator actions.
A release is not considered complete if this information is not available.
## Security and support
Security fixes are integrated by publishing new chart revisions for the relevant supported Stack lines. When a fix is compatible with an existing line, the Stack compatibility line remains the same and only the chart revision changes.
The Formance Platform supports the last two Stack compatibility lines, commonly referred to as `N` and `N-1`. These lines continue to receive compatible fixes, especially security fixes.
If a security fix cannot be applied compatibly to a supported line, Formance may provide a compatible backport, require an upgrade to a more recent Stack line, or document a support exception.
---
## MCP
Source: https://docs.formance.com/mcp
The Formance MCP server exposes selected Formance capabilities to MCP-compatible clients such as coding assistants and local agent runtimes.
It lets an agent inspect financial data and validate Numscript without giving it write access to the stack.
MCP support requires Stack v3.2 or later and `fctl` v3.4.0 or later.
## Enable MCP on a stack
Check your local `fctl` version before enabling MCP:
```bash
fctl version
```
Enable the MCP module on the target stack with `fctl`:
```bash
fctl stack module enable mcp
```
Run this command from an authenticated `fctl` context that targets the organization and stack where MCP should be enabled.
## Run MCP over stdio
MCP clients typically start the server as a local process and communicate with it over standard input and output.
With Formance, `fctl` can serve as the MCP process:
```bash
fctl stack mcp serve --transport=stdio --organization=YOUR_ORGANIZATION_ID --stack=YOUR_STACK_ID
```
`--organization` and `--stack` are only optional when your `fctl` context has exactly one organization and one stack. Otherwise `fctl` exits with `organization not specified` (or `stack not specified`), so pass both explicitly — as above, or as global flags before the subcommand (`fctl --organization=… --stack=… stack mcp serve --transport=stdio`).
Your MCP client configuration should pass the target organization and stack to `fctl`.
The exact configuration format depends on the client:
```json
{
"mcpServers": {
"formance": {
"command": "fctl",
"args": [
"--organization=YOUR_ORGANIZATION_ID",
"--stack=YOUR_STACK_ID",
"stack",
"mcp",
"serve",
"--transport=stdio"
]
}
}
}
```
```toml
[mcp_servers.formance]
command = "fctl"
args = [
"--organization=YOUR_ORGANIZATION_ID",
"--stack=YOUR_STACK_ID",
"stack",
"mcp",
"serve",
"--transport=stdio"
]
```
Replace the `--organization` and `--stack` values with your own environment.
## Verify the server responds
A stdio MCP server has no interactive output: run it on its own and it appears to hang while it waits for JSON-RPC on standard input. That is expected — it is not a failure.
To confirm the server is healthy without an MCP client, pipe an `initialize` request into it:
```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \
| fctl stack mcp serve --transport=stdio --organization=YOUR_ORGANIZATION_ID --stack=YOUR_STACK_ID
```
A healthy server replies with a single JSON line advertising its capabilities:
```json
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{},"tools":{"listChanged":true}},"protocolVersion":"2024-11-05","serverInfo":{"name":"stack-mcp","version":"v0.1.0"}}}
```
If you see an `organization not specified` or `stack not specified` error instead, add the flags shown above. If the command hangs with no reply, check that the `mcp` module is enabled on the stack and that your `fctl` context is authenticated.
## Available capabilities
The MCP server is read-only for Formance data. It is intended for exploration, diagnostics, and assisted development workflows.
| Area | Capabilities |
|------|--------------|
| Ledger | Read ledgers, accounts, balances, volumes, transactions, and related metadata. |
| Payments | Read payments data and inspect payment-related resources. |
| Reconciliation | Read reconciliation data to investigate matching and reconciliation state. |
| Numscript | Validate Numscript before running it against a ledger workflow. |
MCP tools can expose sensitive financial data to the client that starts them. Only configure the Formance MCP server in trusted clients and environments.
## Typical workflow
1. Enable the `mcp` module on a Stack v3.2 or later stack.
2. Configure your MCP client to start `fctl stack mcp serve --transport=stdio`.
3. Ask the client to inspect Ledger, Payments, or Reconciliation state.
4. Use Numscript validation before promoting a script into an operational workflow.
---
## Membership API
Source: https://docs.formance.com/membership-api
The Membership API manages your Formance Cloud resources: organizations, stacks, users, invitations, regions, and access policies.
---
## Stack API Reference
Source: https://docs.formance.com/stack-api-reference
The Formance Platform exposes a unified REST API across all modules. All endpoints use JSON for request and response bodies, and authentication is handled via OAuth2 bearer tokens.
```bash
```
This reference is generated from a Stack release's published specification, and no v4 release has published one. Rather than serve you the **v3.2** specification — 135 endpoints for modules a v4 stack does not compose, and Ledger at `v2` when v4 pins `3.0.0` — the endpoint list is withheld at this version.
For the modules a v4 stack does compose:
- **Ledger 3.0** — the [Ledger documentation](/modules/ledger/get-started) covers the `/v3/` surface, and [HTTP API](/modules/ledger/reference/http-api) locates the contract for the release you run.
- **Auth, Gateway and Search** — v4 composes these unchanged, and they are documented in the latest Stack API reference. That link deliberately leaves the v4 preview; an ordinary link could not, because version context persists across unversioned pages off LATEST.
The endpoint list returns here once a v4 stack release publishes its specification.
---