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.
Make sure to create an API key dedicated to Formance. Doing so will improve your auditability and security and will allow you to revoke access to Formance at any time if needed.
Installation#
curl -X POST $FORMANCE_API_URL/api/payments/v3/connectors/install/bitstamp \
-H "Content-Type: application/json" \
-d @config.jsonWith config.json containing:
{
"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 <apiKey>. |
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 singlesince_idwatermark. - FetchOrders — open-orders snapshot reconciled against
order_status/per tracked id. - FetchConversions —
user_transactions/rows withtype=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 Connectivity 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 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#
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; instant-buy/sell rows (type=36) feed 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:
GetOpenOrdersreturns currently-open orders.- New IDs are seeded into
trackedOrdersstate with their first-sightLimitPrice. GetOrderStatusis called per id (snapshot ∪ tracked) for fills, fees, datetime, and market.- The order maps to a
PSPOrderwith adjustments aggregating each observed state change. - Tracked entries drop on terminal status (
FILLED/CANCELLED). - Tracked entries also drop after
FirstSeenAt + 25d, emittingcom.bitstamp.spec/retention_expired = true. Bitstamp retainsorder_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; 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 inuser_transactions/astype=2withorder_id;MAPPINGS.md §9documents 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.