Migrating from 0.10 to 0.11
The 0.11 release is a breaking vocabulary alignment that profiles every model against an upstream standard (PSD2 / ISO 20022 / Berlin Group / FDX), introduces a polymorphic transfer surface, and pins down a uniform safety contract across mutating tools. This page is a side-by-side map of what changes and how to update.
Field naming: snake_case → camelCase
Every model field on the wire is camelCase in 0.11. The only places snake_case survives are Python tool handler kwargs (bridged to wire by register_tools) and the TransactionsSummary* filters.
| 0.10 (snake_case) | 0.11 (camelCase) |
|---|---|
Transaction.transaction_date | Transaction.bookingDate (also adopts the ISO 20022 / Berlin Group name) |
Transaction.category_id | Transaction.categoryId |
Transaction.amount_in_currency | Transaction.originalAmount |
Transaction.currency (FX-only) | Transaction.originalCurrency |
TransactionsSummaryGroup.category_id / total_amount / transaction_count / average_amount | categoryId / totalAmount / transactionCount / averageAmount |
TransactionsSummaryPeriod.start_date / end_date | startDate / endDate |
Transaction shape
// 0.10
{
"id": "tx_001",
"description": "Monthly Salary",
"amount": 4500.00,
"transaction_date": "2024-03-15",
"category_id": "cat_income"
}
// 0.11 — required fields
{
"id": "tx_001",
"accountId": "acc_checking_001", // now required: links to Account.id
"description": "Monthly Salary",
"amount": 4500.00,
"bookingDate": "2024-03-15"
}
idis now required (was optional).accountIdis a new required field linking each transaction to itsAccount.statusis optional and defaults to"Booked"when omitted (profile of Berlin GroupbookingStatus).counterpartyNameis a new optional best-effort merchant / counterparty display string.
New optional ISO 20022 fields
Servers populate what they have; the None-omitting serializer drops the rest, so an empty server costs zero bytes.
| Field | Profile | When to populate |
|---|---|---|
valueDate | ISO 20022 ValueDate | Settlement date if it differs from bookingDate |
categoryRaw | bank-native | When categoryId was mapped from a more specific raw label |
counterparty | typed Party | When more than just counterpartyName is known |
transactionCode | ISO 20022 BankTransactionCode (domain / family? / subFamily?) | When the bank exposes the taxonomy |
remittanceInformation | ISO 20022 RemittanceInformation | When remittance info is not just a duplicate of description |
endToEndId | ISO 20022 end-to-end identifier | When the rail carries one |
merchantCategoryCode | ISO 18245 MCC (4 digits) | Card transactions on rails that expose the MCC |
Verbosity caps how many optional fields land
get-transactions now takes a verbosity parameter (minimal | standard | full, default standard) that caps the optional fields each Transaction may carry. Servers MAY still omit anything they don't have, even at full.
// minimal — required only plus counterpartyName
{ "id", "accountId", "description", "amount", "bookingDate", "counterpartyName?" }
// standard (default) — adds the Berlin-Group-style fields
{ ..., "status?", "categoryId?", "originalCurrency?", "originalAmount?" }
// full — adds every optional ISO 20022 field
{ ..., "valueDate?", "categoryRaw?", "counterparty?", "transactionCode?",
"remittanceInformation?", "endToEndId?", "merchantCategoryCode?" }
get-transaction (singular) for detail-on-demand
# Audit / reconciliation flow: lookup by id, always at full verbosity
response = await client.call_tool(
"get-transaction",
{"transaction_id": "tx_001"},
)
Categories: canonical id taxonomy
Servers SHOULD use one of the 14 canonical Category.id values when a category maps cleanly:
Income, Transfer, Groceries, DiningAndEntertainment, Transport, Housing,
Utilities, Shopping, Health, Travel, Subscriptions, Fees, Cash, Other
Non-canonical ids remain valid for server-specific extensions; clients MUST treat any Category.id as opaque.
Recipient shape
// 0.10
{
"id": "rcpt_001",
"name": "Jane Doe",
"accountNumber": "5678-90-123456",
"accountNumberType": "Domestic",
"bankInfo": "Demo Bank",
"paymentType": "Domestic",
"socialSecurityNumber": "123-45-6789", // required
"isFavorite": true,
"description": "Friend"
}
// 0.11
{
"id": "rcpt_001",
"name": "Jane Doe",
"accountIdentifier": { // typed, replaces accountNumber + accountNumberType
"type": "accountNumber",
"accountNumber": "5678-90-123456",
"country": "US",
"routing": "021000021"
},
"nationalId": { // typed, optional (replaces required socialSecurityNumber)
"value": "123-45-6789",
"country": "US",
"type": "ssn"
},
"nickname": "Friend",
"isFavorite": true
}
accountNumber+accountNumberTypecollapse into the typedaccountIdentifierdiscriminated union with four variants:iban,bban,accountNumber,alias.socialSecurityNumber(required) becomesnationalId(optional, typed object).typeis an opaque label (kennitala,ssn,cpr,personnummer,cpf,other); bank2ai does not validate the value.- Dropped:
bankInfo,paymentType,address,description. Added:nickname,bic,defaultDescription,lastUsedAt. - The
RecipientInfoPython base class is removed.
create-recipient inputs
# 0.10
await client.call_tool("create-recipient", {
"name": "Jane Doe",
"account_number": "5678-90-123456",
"kennitala": "010190-1234",
})
# 0.11
await client.call_tool("create-recipient", {
"name": "Jane Doe",
"account_identifier": {
"type": "bban",
"bban": "0133-26-007890",
"country": "IS",
},
"national_id": {
"value": "010190-1234",
"country": "IS",
"type": "kennitala",
},
"nickname": "Friend", # optional
"idempotency_key": "client-recipient-001", # optional
})
Transfer surface: prepare-transfer is polymorphic; execute-transfer is intent-id-only
# 0.10 — Icelandic-specific tool, execute takes the full details again
prepared = await client.call_tool("prepare-transfer-icelandic", {
"amount": 100.0,
"recipient_ssn": "010190-1234",
"recipient_account_number": "0133-26-007890",
"description": "Lunch share",
})
await client.call_tool("execute-transfer", {
"withdrawal_account_id": "acc_checking_001",
"recipient_account_number": "0133-26-007890",
"amount": 100.0,
"description": "Lunch share",
})
# 0.11 — polymorphic prepare-transfer with rail; execute takes only the intent id;
# prepare-transfer-icelandic is removed
prepared = await client.call_tool("prepare-transfer", {
"debtor_account_id": "acc_checking_001",
"creditor": {
"name": "Jón Jónsson",
"accountIdentifier": {
"type": "bban",
"bban": "0133-26-007890",
"country": "IS",
},
"nationalId": {
"value": "010190-1234",
"country": "IS",
"type": "kennitala",
},
},
"amount": 100.0,
"currency": "ISK",
"rail": "domestic-IS",
"description": "Lunch share",
})
intent_id = prepared["item"]["transferIntentId"]
await client.call_tool("execute-transfer", {"transfer_intent_id": intent_id})
The Rail enum ships with domestic-IS, sepa, sepa-instant, swift; servers MAY register more values via vendor extensions.
prepare-transfer-icelandic is no longer registered. Replace any call site with prepare-transfer and rail=domestic-IS.
PreparedTransfer envelope
{
"content": "A transfer has been prepared. ...",
"item": {
"transferIntentId": "intent_...",
"expiresAt": "2024-03-15T08:35:00Z", // 5 min default
"fees": [{ "amount": 0.50, "currency": "EUR", "description": "SEPA fee" }],
"fx": { /* present on cross-currency transfers */ },
"confirmationOfPayee": {
"status": "match" // or close-match | no-match | unavailable
},
"warnings": [],
"summary": {
// validated, normalised echo of the inputs
"debtorAccount": { /* Account snapshot */ },
"creditor": { /* Party */ },
"amount": 100.0,
"currency": "ISK",
"rail": "domestic-IS",
"endToEndId": "e2e_...", // always populated; server-generated if not provided
"description": "Lunch share"
}
}
}
Safety contract on mutating tools
Every mutating tool (create-recipient, prepare-transfer, execute-transfer) accepts an optional idempotency_key (≤128 chars). Servers SHOULD return the original response for repeat calls with the same key within at least 24 hours. The key is scoped per (tool, caller); two unrelated callers cannot collide.
The mutating-tool response envelopes (CreateRecipientResponse, PrepareTransferResponse, ExecuteTransferResponse) carry an optional structured code field on recoverable errors. Canonical values: intent_not_found, intent_expired, missing_creditor_identifier, insufficient_funds, invalid_account, invalid_recipient. Unknown codes MUST be treated as opaque.
execute-transfer rejects expired intents with code: "intent_expired"; the intent's expiresAt is 5 minutes after prepare-transfer by default. The intent's amount, creditor, debtor, and rail are immutable — any change requires a fresh prepare-transfer call.
Account: typed balances array (additive)
{
"id": "acc_checking_001",
"balance": 5420.50, // required, derived shortcut for ClosingBooked
"availableBalance": 5420.50, // optional, derived shortcut for InterimAvailable
"balances": [ // new, optional
{ "type": "ClosingBooked", "amount": 5420.50, "currency": "USD", "asOf": "2024-03-15T08:30:00Z" },
{ "type": "InterimAvailable", "amount": 5420.50, "currency": "USD", "asOf": "2024-03-15T08:30:00Z" }
]
}
The top-level scalars stay as derived shortcuts; servers MUST keep them consistent with the corresponding balances entries when both are present. Servers that only have the scalars omit balances and the None-omitting serializer drops it from the wire payload.
Field omission and tolerance
The spec preamble now documents the lean-payload rule explicitly: servers SHOULD omit optional fields whose values are null, empty, or equal to a documented default; clients MUST tolerate missing optional fields and unknown additional fields. The Python reference implementation does this automatically via the _Bank2aiModel base class. Required fields are unaffected.
Checklist for updating a server
- Bump
bank2aidependency to>=0.4.0. - Migrate model field names to camelCase in any handler that constructs
TransactionorTransactionsSummary*objects. - Make
Transaction.idandTransaction.accountIdpopulated on every transaction your server emits. - Update
create-recipienthandler signature:account_identifier,national_id,nickname,bic,default_description,idempotency_key(replacingaccount_numberandkennitala). - Replace any
prepare_transfer_icelandichandler with a polymorphicprepare_transferhandler. - Replace your
execute_transferhandler to take onlytransfer_intent_id(and optionalidempotency_key); add a short-lived intent store keyed bytransferIntentId. - (Optional) Map your bank's category labels onto the canonical
CANONICAL_CATEGORY_IDStaxonomy. - (Optional) Populate
Transactionoptional fields (status,counterparty,transactionCode, ...) andAccount.balancesfrom your upstream data when available.
Checklist for updating a client
- Read Transaction fields with the new camelCase names (
bookingDate,categoryId,originalCurrency,originalAmount). - When listing transactions, set
verbositytominimalfor compact UIs, leave it atstandardfor general use, or usefullonly when the agent actually needs ISO 20022 metadata. - Use
get-transaction(singular) for detail / audit views rather than asking forverbosity: "full"on a list. - For new transfer flows, call
prepare-transferwith a typedcreditorPartyand arail; the response'stransferIntentIdis the only thingexecute-transferneeds. - Generate an
idempotency_keyper logical user action (e.g., one UUID per "send money" press) and pass it on every mutating call. - Branch on the structured
codefield on recoverable errors rather than parsingcontent.