The Bitstamp connector polls a Bitstamp account and surfaces its currency wallets, balances, payments, trading orders, and conversions to the Connectivity service. It is read-only and spot-only.
Bitstamp API keys are scoped to a single account — Main, or one named sub-account — so this connector follows a one-install-per-scope model. The X-Auth-Subaccount-Id header was confirmed non-functional through live probing; reconciling activity across multiple sub-accounts means installing one connector per scope and joining results downstream.
Prerequisites#
You need a Bitstamp account and an API key dedicated to Formance, with the least permissions required for the capabilities you plan to use. Bitstamp uses HMAC-SHA256 v2 signing — every request carries five headers (X-Auth, X-Auth-Signature, X-Auth-Nonce, X-Auth-Timestamp, X-Auth-Version: v2). The connector handles the signing 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/connectors/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 the X-Auth header as BITSTAMP <apiKey>. |
apiSecret | yes | — | HMAC-SHA256 signing secret. Never logged. |
endpoint | no | https://www.bitstamp.net | API root. Override only for non-production environments. |
name | yes | — | A unique name for this connector instance. Useful when running one connector per Bitstamp account scope (e.g. bitstamp-main, bitstamp-treasury). |
pollingPeriod | no | 30m | Sync cadence (minimum 20m). Drives every capability — Accounts, Balances, Payments, Orders, Conversions. |
The config is deliberately minimal. There is no accountScope or subAccountId flag — the API key already scopes the connection, and Bitstamp's API exposes no portable way to fan out across scopes. Adding a scope flag would let a misconfigured install claim to be Main while authenticating as a sub-account.
Capabilities#
The Bitstamp connector exposes the following capabilities:
- FetchAccounts — currency wallets in the account scope, via
POST /api/v2/account_balances/. - FetchBalances — derived from the
PSPAccount.Rawsnapshot already returned by Accounts; no second API call. - FetchPayments — three-source union over
user_transactions/,crypto-transactions/(Main-only), andwithdrawal-requests/. - FetchOrders — open-orders snapshot reconciled against
order_status/per tracked id. - FetchConversions —
user_transactions/rows withtype=36(instant buy/sell, atomic two-asset swaps).
Payouts, transfers, webhooks, and bank-account creation are not implemented today — Bitstamp's API surface for those flows is uneven and not yet covered.
Workflow tree#
Four periodic root tasks, each with its own cursor, advance independently:
FetchAccounts (periodic)
└── FetchBalances (FromPayload — no extra API call)
FetchPayments (periodic root)
FetchOrders (periodic root)
FetchConversions (periodic root)Bitstamp's payments / orders / conversions endpoints are account-global at the API-key level, so no parent-account context is needed for those roots. FetchBalances runs nested under FetchAccounts because the balance values are already present in the account_balances/ response — calling a second endpoint would be wasted work.
Account model#
Every internal account in Connectivity corresponds to a single currency in the Bitstamp account scope — one PSPAccount per (connector install, currency). The reference is the currency ticker (USD, EUR, BTC); the connector-level name (e.g. bitstamp-main) disambiguates the scope, so the per-currency name only needs to carry the ticker.
Bitstamp returns every currency the account could hold, even with all values at zero. Rows where Available, Total, and Reserved are all zero are skipped at the orchestrator — emitting hundreds of empty accounts pollutes the catalogue without informing anyone.
Bitstamp does not expose per-currency creation dates, so CreatedAt defaults to BitstampGenesis = 2011-08-02 UTC (the platform's launch date). The sentinel is stable across reinstalls, which keeps the field meaningful even though it isn't truthful.
Asset model#
The canonical asset string is the currency ticker, uppercased, with the precision suffix from Bitstamp's currencies cache — USD/2, EUR/2, BTC/8, USDT/6. The connector loads the cache at install time and refreshes it on a TTL; assets not in the cache are logged and skipped rather than emitted with a guessed precision.
Payments — three-source union#
A Formance Payment originates from one of three Bitstamp endpoints, fanned in by the FetchPayments orchestrator. Each source has its own ID space and its own cursor; the engine dedupes the union by (source, id). Every payment carries com.bitstamp.spec/source so downstream consumers can tell where a row came from.
| Source | Scope | Cursor | What it carries |
|---|---|---|---|
user_transactions/ | Main + sub | since_id watermark on tx.ID | Settled fiat / crypto activity, deposits, payouts, sub-account transfer legs (types 14 / 33 / 35). Excludes type=2 trades and type=36 instant buy/sell — those feed Orders and Conversions respectively. |
crypto-transactions/ | Main only | Per-bucket datetime Unix-seconds (deposits, withdrawals, ripple IOUs each track separately) | On-chain crypto flows with explicit network + txid + destination address. Sub-account scopes hit the try-and-skip cache — see Install-time enrichment. |
withdrawal-requests/ | Main + sub | id-based after a cold-start offset walk | Fiat withdrawal lifecycle with status enum (PENDING / SUCCEEDED / CANCELLED / FAILED) and an explicit scheme (SEPA, wire, ACH, crypto). |
user_transactions/ watermarks are inclusive — the last row of cycle N reappears as the first row of cycle N+1, deduped downstream by PSPPayment.Reference. End-of-pagination keeps the watermark; we never reset.
Sub-account transfers#
When a transfer crosses two scopes inside the same Bitstamp account, the connector emits each leg as a signed payment with a shared correlation key:
| Leg | Type | Amount | transfer_pair_id | transfer_direction |
|---|---|---|---|---|
| Source side | PAYOUT | abs(amount) | tx.id | outgoing |
| Destination side | PAYIN | amount | tx.id (identical) | incoming |
Why not a single TRANSFER row? Each Bitstamp connector sees only one side of a sub-account transfer (its API key scopes it that way). The Formance PSPPayment model expects a single connector to populate both account references; it can't, so the payout / payin pair is the only model that stays internally consistent. Downstream consumers reconstruct the full transfer by joining (transfer_pair_id, asset).
Live-probed reality: a Main-account API key does NOT actually surface type-14 / 33 / 35 rows in user_transactions/ today, even when the web UI shows transfers. The mapping above stays as defensive code that activates the moment Bitstamp exposes the rows on any polled endpoint. Customers who need transfer reconciliation install one connector per sub-account; the pair-id correlation works once both legs' API keys are integrated.
Orders#
Bitstamp does not expose an "orders since X" endpoint. The connector reconciles a live snapshot every cycle:
GetOpenOrdersreturns the currently-open orders.- New IDs are seeded into the
trackedOrdersstate with their first-sightLimitPrice. GetOrderStatusis called per id (snapshot ∪ tracked) for the rich shape — fills, fees, datetime, market.- The order is mapped to a
PSPOrder, with the adjustments list aggregating each observed state change. - Tracked entries drop on terminal status (
FILLED/CANCELLED). - Tracked entries also drop after
FirstSeenAt + 25d, withcom.bitstamp.spec/retention_expired = trueon the final emission. Bitstamp retainsorder_status/rows for 30 days; the 5-day safety margin avoids losing an order's terminal state to retention.
The Trade primitive that Bitstamp surfaces in user_transactions/ (type=2 rows carrying a parent order_id) is aggregated under its parent order rather than emitted as a standalone Order. One PSPOrder per Bitstamp order, fills attached.
Conversions#
Bitstamp returns two primitives in user_transactions/ that both look like "buys" and "sells" in the web UI. The reliable distinction:
| 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 |
The conversions task shares the user_transactions/ stream with payments but holds its own watermark, so the two cursors advance independently. Asset class plays no role in the classification — Bitstamp tags every crypto (BTC, USDC, EURC, …) as currency.type = "crypto" with no native stablecoin tag. A type=36 BTC↔EUR row and a type=36 USDC↔EUR row are the same primitive (spot-priced atomic swap); downstream consumers wanting "market exposure" vs "stable-value swap" semantics apply their own stablecoin allow-list against SourceAsset / DestinationAsset.
Install-time enrichment#
The connector loads four reference datasets in parallel at install time, refreshed via a TTL cache:
markets— every trading pair Bitstamp offers, used to resolve order quote / base currencies.my_markets— pairs the API key has actually traded (gates Order details to permitted markets).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 is logged once at Info level, then subsequent attempts on the same key are silenced. This keeps logs readable when an install runs against a read-only key without trading scope.
Metadata keys#
Every Bitstamp-specific field lives under the com.bitstamp.spec/ namespace. The full inventory lives in the connector's MAPPINGS.md; the operationally useful highlights:
- Account:
currency_type,currency_decimals,withdrawal_fee?,is_crypto?. - Payment:
source(one ofuser_transactions,crypto_transactions,withdrawal_requests), plus source-specific keys —network,txid,destination_address,bank_transaction_id?,pending_reason?,transfer_pair_id?,transfer_direction?. - 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#
Each source carries an opaque cursor in connector state. The watermark is immutable for the duration of a cycle — advancing mid-cycle would create a page-2-tighter-than-page-1 race that drops rows whose timestamp lands between page boundaries. The orchestrator persists the cursor only after the cycle completes successfully.
paymentsState is a 3-way structure (one cursor per payments source) with a backward-compatible decoder for installs that ran the legacy single-watermark version of the connector — no manual migration step.
Known gaps#
- Historical orders. Orders placed and fully filled before the connector was installed, or older than 30 days, fall outside Bitstamp's
order_status/retention and are not back-filled today. The per-fill rows exist inuser_transactions/astype=2withorder_id; aggregating them byorder_idis the planned path. Tracked as future work inMAPPINGS.md §9. - No programmatic sub-account discovery. Bitstamp's API doesn't expose a "list my scopes" call. The deployment model is one connector per account scope, named explicitly via the
nameconfig field. - Cross-account transfer rows. Defensive mapping is in place (see Sub-account transfers) but the underlying API rows aren't surfaced today. Customers needing transfer reconciliation install one connector per sub-account.