sondahub

APIs / Bank

Bank

Retail banking: customers, accounts, cards, eight thousand transactions, transfers and FX rates.

Accounts carry real running balances. POST a transfer between two accounts and the hub debits one, credits the other and writes both transactions — in the answer; the data itself never moves. FX rates tick over the live stream. 10,047 records in all.

Connect

Base URL
https://api.sondahub.com/v1/bank
OpenAPI 3
https://api.sondahub.com/v1/bank/openapi.json
GraphQL
https://api.sondahub.com/v1/bank/graphql
WebSocket
wss://api.sondahub.com/v1/bank/ws
SSE
https://api.sondahub.com/v1/bank/events
The data
customers.json, accounts.json, cards.json, transactions.json, transfers.json, fx_rates.json

In Sonda: Import → From a URL with the OpenAPI address and the whole API lands as a project, one request per operation with example bodies. No keys, no headers to add. More on each protocol.

Writes here are simulated: POST, PUT, PATCH and DELETE are validated, run through the real logic and answered as a real server would — then forgotten. The answer carries _note and X-Sondahub-Write: simulated. A GET afterwards will not find what you wrote.
The API describes itself
curl https://api.sondahub.com/v1/bank

Lists and filters

Every list answers { "data": [...], "meta": { "page", "limit", "total", "pages" } } with X-Total-Count and Link headers (next, prev, first, last). These options work on every collection and every nested route:

OptionMeaningExample
page, limitPaging, 1-based; limit 1–200, default 20. offset works too.?page=3&limit=50
sortComma list of fields, - for descending. Default id here.?sort=-risk_score,id
field=valueEquals. Booleans as true/false, null for missing.?segment=retail
_ne _gt _gte _lt _lteNot equal and comparisons, on numbers, dates and strings.?risk_score_gte=10&risk_score_lt=100
_likeContains, case-insensitive.?name_like=an
_inAny of a comma list.?id_in=1,2,3
_nulltrue: missing; false: present.?phone_null=true
a.b=valueInside a JSON field, dotted.?address.line1=…
qSearch across the text fields.?q=alpine
fieldsOnly these fields back.?fields=id,name
expandEmbed related records.?expand=accounts,cards

A name that is not a field answers 400 and lists the fields. Writes answer 422 with one line per problem, 404 for a missing id, 405 with an Allow header for a verb a route does not take.

customers

Account holders. 400 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
namerequiredstring
emailrequiredstring
phonestring
date_of_birthdate
addressjson { line1, line2?, city, region, postal_code, country }
segmentenumretail premium business student
kyc_statusenumpending verified rejected
risk_scoreintmin 0, max 100

Relations: accounts → the accounts whose customer_id is this customer; cards → the cards whose customer_id is this customer. Use ?expand=accounts,cards to embed them, or the routes below.

Endpoints

GET/v1/bank/customersA page, with every filter, sort, search, field and expand option below.
POST/v1/bank/customersCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/bank/customers/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/bank/customers/{id}Change the fields you send.
PUT/v1/bank/customers/{id}Replace the record; required fields must all be there.
DELETE/v1/bank/customers/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/bank/customers/{id}/accountsIts accounts, as a page with all the list options.
GET/v1/bank/customers/{id}/cardsIts cards, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/bank/customers?segment=premium&risk_score_gte=1&limit=3"
One record
curl https://api.sondahub.com/v1/bank/customers/1
Its accounts
curl "https://api.sondahub.com/v1/bank/customers/1/accounts?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/bank/customers \
  -H "Content-Type: application/json" \
  -d '{"name":"A name","email":"A email","segment":"retail","kyc_status":"pending"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/bank/customers/1 \
  -H "Content-Type: application/json" \
  -d '{"segment":"premium"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/bank/customers/1

accounts

Checking, savings, credit and loan accounts. balance is the current balance after every transaction. 640 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
numberread-onlystringUnique account number.
ibanread-onlystring
customer_idrequiredint → customers
typerequiredenumchecking savings credit loan
nicknamestring
currencystring
balancefloatCurrent balance. Negative on credit and loan accounts means money owed.
availablefloatBalance minus holds, or credit left.
credit_limitfloatCredit accounts only.
interest_ratefloatAnnual, percent.
statusenumactive frozen closed
opened_atdate

Relations: customer → one customer through customer_id; transactions → the transactions whose account_id is this account; cards → the cards whose account_id is this account. Use ?expand=customer,transactions,cards to embed them, or the routes below.

Endpoints

GET/v1/bank/accountsA page, with every filter, sort, search, field and expand option below.
POST/v1/bank/accountsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/bank/accounts/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/bank/accounts/{id}Change the fields you send.
PUT/v1/bank/accounts/{id}Replace the record; required fields must all be there.
DELETE/v1/bank/accounts/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/bank/accounts/{id}/customerThe customer this record points at.
GET/v1/bank/accounts/{id}/transactionsIts transactions, as a page with all the list options.
GET/v1/bank/accounts/{id}/cardsIts cards, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/bank/accounts?type=savings&balance_gte=10&expand=customer&limit=3"
One record
curl https://api.sondahub.com/v1/bank/accounts/1?expand=customer
Its transactions
curl "https://api.sondahub.com/v1/bank/accounts/1/transactions?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/bank/accounts \
  -H "Content-Type: application/json" \
  -d '{"customer_id":1,"type":"checking","nickname":"Everyday","currency":"USD","status":"active"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/bank/accounts/1 \
  -H "Content-Type: application/json" \
  -d '{"type":"savings"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/bank/accounts/1

cards

Debit and credit cards on an account. Numbers are masked; the last four are real digits of the seed. 505 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
account_idrequiredint → accounts
customer_idrequiredint → customers
brandenumvisa mastercard amex
typeenumdebit credit virtual
masked_numberread-onlystring
last4read-onlystring
holder_namestring
expiresstring
statusenumactive blocked expired lost
contactlessbool
daily_limitfloatmin 0

Relations: account → one account through account_id; customer → one customer through customer_id. Use ?expand=account,customer to embed them, or the routes below.

Endpoints

GET/v1/bank/cardsA page, with every filter, sort, search, field and expand option below.
POST/v1/bank/cardsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/bank/cards/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/bank/cards/{id}Change the fields you send.
PUT/v1/bank/cards/{id}Replace the record; required fields must all be there.
DELETE/v1/bank/cards/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/bank/cards/{id}/accountThe account this record points at.
GET/v1/bank/cards/{id}/customerThe customer this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/bank/cards?brand=mastercard&daily_limit_gte=10&expand=account&limit=3"
One record
curl https://api.sondahub.com/v1/bank/cards/1?expand=account
Create (simulated)
curl -X POST https://api.sondahub.com/v1/bank/cards \
  -H "Content-Type: application/json" \
  -d '{"account_id":1,"customer_id":1,"brand":"visa","type":"debit","expires":"09/28","status":"active"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/bank/cards/1 \
  -H "Content-Type: application/json" \
  -d '{"brand":"mastercard"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/bank/cards/1

transactions

Every movement on an account. amount is signed: negative leaves the account. balance_after is the running balance. 7,674 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
account_idrequiredint → accounts
referenceread-onlystringUnique.
typeenumcard transfer deposit withdrawal fee interest payment refund
amountrequiredfloat
currencystring
balance_afterread-onlyfloat
descriptionstring
merchantstring
categoryenumgroceries dining transport fuel shopping utilities entertainment health travel subscriptions transfer income fees other
statusenumpending posted reversed
card_idint → cards
counterpartyjson { name, account_number? }
booked_atrequireddatetime
value_datedate

Relations: account → one account through account_id; card → one card through card_id. Use ?expand=account,card to embed them, or the routes below.

Endpoints

GET/v1/bank/transactionsA page, with every filter, sort, search, field and expand option below.
POST/v1/bank/transactionsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/bank/transactions/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/bank/transactions/{id}Change the fields you send.
PUT/v1/bank/transactions/{id}Replace the record; required fields must all be there.
DELETE/v1/bank/transactions/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/bank/transactions/{id}/accountThe account this record points at.
GET/v1/bank/transactions/{id}/cardThe card this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/bank/transactions?type=transfer&amount_gte=10&expand=account&limit=3"
One record
curl https://api.sondahub.com/v1/bank/transactions/1?expand=account
Create (simulated)
curl -X POST https://api.sondahub.com/v1/bank/transactions \
  -H "Content-Type: application/json" \
  -d '{"account_id":1,"type":"card","amount":9.99,"category":"groceries","status":"pending","booked_at":"2026-09-30T12:00:00Z"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/bank/transactions/1 \
  -H "Content-Type: application/json" \
  -d '{"type":"transfer"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/bank/transactions/1

transfers

Money moving between two accounts. POST {"from_account_id","to_account_id","amount","description"} and the hub checks the funds, debits, credits, and writes both transactions. Insufficient funds answers 422. 800 records — the file.

POST checks both accounts exist, are active, hold the same currency and that the amount is available, then debits, credits and writes a transaction on each side; the answer carries debit_transaction_id and credit_transaction_id. Anything wrong answers 422 with the reason.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
from_account_idrequiredint → accounts
to_account_idrequiredint → accounts
amountrequiredfloatmin 0.01
currencystring
descriptionstring
statusread-onlyenumcompleted pending failed reversed
debit_transaction_idread-onlyint → transactions
credit_transaction_idread-onlyint → transactions
scheduled_fordate
executed_atread-onlydatetime

Relations: from_account → one account through from_account_id; to_account → one account through to_account_id. Use ?expand=from_account,to_account to embed them, or the routes below.

Endpoints

GET/v1/bank/transfersA page, with every filter, sort, search, field and expand option below.
POST/v1/bank/transfersCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/bank/transfers/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/bank/transfers/{id}Change the fields you send.
PUT/v1/bank/transfers/{id}Replace the record; required fields must all be there.
DELETE/v1/bank/transfers/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/bank/transfers/{id}/from_accountThe account this record points at.
GET/v1/bank/transfers/{id}/to_accountThe account this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/bank/transfers?status=pending&amount_gte=10&expand=from_account&limit=3"
One record
curl https://api.sondahub.com/v1/bank/transfers/1?expand=from_account
Create (simulated)
curl -X POST https://api.sondahub.com/v1/bank/transfers \
  -H "Content-Type: application/json" \
  -d '{"from_account_id":1,"to_account_id":3,"amount":100,"description":"Rent"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/bank/transfers/1 \
  -H "Content-Type: application/json" \
  -d '{"amount":42.5}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/bank/transfers/1

fx rates

Exchange rates against USD and the main crosses. The live stream ticks them. 28 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
pairrequiredstring
baserequiredstring
quoterequiredstring
raterequiredfloat
bidfloat
askfloat
change_24h_pctfloat
as_ofdatetime

Endpoints

GET/v1/bank/fx_ratesA page, with every filter, sort, search, field and expand option below.
POST/v1/bank/fx_ratesCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/bank/fx_rates/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/bank/fx_rates/{id}Change the fields you send.
PUT/v1/bank/fx_rates/{id}Replace the record; required fields must all be there.
DELETE/v1/bank/fx_rates/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).

Try it

List with a filter
curl "https://api.sondahub.com/v1/bank/fx_rates?rate_gte=10&limit=3"
One record
curl https://api.sondahub.com/v1/bank/fx_rates/1
Create (simulated)
curl -X POST https://api.sondahub.com/v1/bank/fx_rates \
  -H "Content-Type: application/json" \
  -d '{"pair":"USD/ARS","base":"USD","quote":"ARS","rate":9.99}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/bank/fx_rates/1 \
  -H "Content-Type: application/json" \
  -d '{"rate":42.5}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/bank/fx_rates/1

WebSocket and SSE

The same stream two ways: the world's own activity, one tick a second, generated for your connection alone. Both push JSON text messages; SSE names each one with event: and numbers it with id:. ?topics=a,b narrows either.

TopicWhat arrivesHow often
fxA rate ticking: pair, rate, bid, ask.1 s
transactionsA card payment posting on one of the seed accounts.3 s
WebSocket
wss://api.sondahub.com/v1/bank/ws?topics=fx

> {"type":"hello","api":"bank","topics":[…],"subscribed":[…]}
> {"type":"event","topic":"fx","api":"bank","ts":"…","data":{…}}
< {"type":"subscribe","topics":["fx"]}   # narrow to some topics
< {"type":"ping"}                            # → {"type":"pong"}
< anything else                             # → echoed back as {"type":"echo"}
Server-Sent Events
curl -N "https://api.sondahub.com/v1/bank/events?topics=fx"

retry: 3000
id: 1
event: fx
data: {"type":"event","topic":"fx",…}

GraphQL

One endpoint, https://api.sondahub.com/v1/bank/graphql: POST {"query", "variables"} or GET ?query=. Introspection is on, so Sonda's GraphQL mode loads the schema; the SDL is a click away. Every collection is a paged query with the same filter, sort and q options as REST (operators as suffixes: price_lt), a by-id query, relation fields both ways, and create, update, replace and delete mutations — simulated like every write, with the note in extensions.

A query
curl https://api.sondahub.com/v1/bank/graphql -H "Content-Type: application/json" -d '{"query": "{ accounts(limit: 3, sort: \"-id\", filter: { type: checking }) { total data { id number iban customer_id customer { name } transactions(limit: 2) { id } } } }"}'
{
  accounts(limit: 3, sort: "-id", filter: { type: checking }) {
    total
    data {
      id number iban customer_id
      customer { name }
      transactions(limit: 2) { id }
    }
  }
}