API conventions
A handful of patterns hold across every endpoint in the API. Learn them once here and each individual resource in the reference will read the same way — the same auth, the same money format, the same timestamps, the same list shape.
Authentication
Every request carries an OAuth 2.0 bearer token in the Authorization header. Tokens are short-lived and workspace-scoped. See the Authentication guide for the token exchange and refresh flow.
Authorization: Bearer $VB_TOKEN
Retrying a write
The create endpoints — POST /v1/orders, POST /v1/deposits, and POST /v1/withdrawals — support a real idempotency contract: send an Idempotency-Key header and a retry of the same request returns the original result instead of creating a duplicate. Resource ids are always generated by VirtuaBroker and never derived from your key.
- Key format — 1 to 128 characters of letters, digits, dash or underscore, starting with a letter or digit, sent exactly once per request. A malformed key is rejected with
400before anything happens — never silently ignored. - Scope — keys are scoped to your workspace, account and endpoint. Reusing your key on a different endpoint is rejected with
409. - Replay — the same key with the same payload returns the original response, and exactly one resource ever exists. The same key with a different payload is rejected with
409. - Concurrency — two simultaneous identical requests produce exactly one resource; while the first is still executing the retry answers the same response or
409(in progress). - A failed attempt consumes the key — if a keyed request is accepted and then fails during execution, further requests with that key answer
409: verify the current state, then use a new key. Only a400from request validation leaves the key unused, because it is refused before anything is reserved; a400raised while executing an already-accepted request — an amount outside the allowed range, for example — consumes the key like any other execution failure. - TTL — a key whose attempt succeeded replays for 24 hours; after that, the same key starts a fresh request. A key burned by a failed attempt is never released by that expiry: only the operator releases it, after verifying what the failed attempt did.
# one fresh key per logical attempt; retry a timeout with the SAME key curl -X POST "https://api.virtuabroker.com/v1/deposits" \ -H "Authorization: Bearer $VB_TOKEN" \ -H "Idempotency-Key: 8f3c1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b" \ -H "Content-Type: application/json" \ -d '{ "amount": 100, "currency": "EUR", "userData": { "userEmail": "payer@example.com", "firstName": "Ana", "userType": "individual", "country": "ES" } }'
POST /v1/aml-screenings and the conversation mutations accept the header with their own, narrower semantics — see their sections.
Keyless writes still do not de-duplicate. If a create without an Idempotency-Key times out, reconcile before resending: list the resource filtered by userEmail plus a from/to window (and currency), and check whether your operation already exists. Resend only if it does not. The same reconciliation is how you verify state after a keyed attempt whose outcome is unknown, before minting a new key.
# after a timed-out create, check before resending curl "https://api.virtuabroker.com/v1/deposits?userEmail=payer@example.com&from=1780000000&to=1780003600¤cy=EUR" \ -H "Authorization: Bearer $VB_TOKEN"
On POST /v1/orders only, set externalReference on the create to carry your own order number. Order reads return it as externalRefeference — note the spelling — so that is the field to match on when you reconcile an order.
POST /v1/deposits and POST /v1/withdrawals do not accept an externalReference; it is ignored if sent. Reconcile those on userEmail plus the time window, currency and amount.
Amounts & currencies
Monetary amounts are JSON numbers, not strings — sending an amount as a string fails validation. Currencies are ISO 4217 alphabetic codes (EUR, BRL, USD, …). Every amount is paired with the currency it is denominated in.
{
"originAmount": 1000,
"originCurrency": "EUR",
"destinationAmount": 6098.2,
"destinationCurrency": "BRL"
}
Rails & methods
Payin methods, payout methods, and rails are identifiers you discover per currency rather than hard-code. Fetch the accepted shapes with GET /v1/currencies/{currency}/payin-schema and GET /v1/currencies/{currency}/destination-schema — the API reference documents both.
Public rail names such as SEPA, PIX, and SPEI appear in requests and responses where they identify the rail. The API never exposes the internal providers that clear a given rail.
Timestamps
Timestamps are numeric Unix epoch values, not ISO strings. A quote's expireAt is Unix seconds; resource timestamps such as createdAt and updatedAt are numeric as well, and the list filters from and to take Unix values. Parse them as instants; do not assume a local timezone.
{
"expireAt": 1694291200
}
Pagination
List endpoints accept limit and page query parameters and return a bare JSON array — there is no wrapper object and no total in the response.
[
{ /* order */ },
{ /* … up to 20 orders … */ }
]
Totals come from separate count endpoints: GET /v1/orders/count and GET /v1/deposits/count, each returning { "count": … }. There is no count endpoint for withdrawals.
{
"count": 137
}
Lists also accept filters: currency, minAmount, maxAmount, from (Unix), to (Unix), userEmail, search, limit, and page — plus status on the order, deposit, and withdrawal list endpoints.
Rate limits
Read endpoints (GET) are throttled, not rejected: past a threshold within the window, responses gain added latency. No error is returned — there is no 429.
Write endpoints — POST /v1/orders, POST /v1/deposits, and POST /v1/withdrawals — are limited per workspace, at roughly 300 requests per minute. Exceeding the limit returns HTTP 503 with OperationError, and responses carry the standard RateLimit-* headers. There is no Retry-After header and no X-RateLimit-* headers.
The API never returns 429. Handle the write limit by treating a 503 OperationError as retryable — back off and resend. A rate-limited request was rejected before it created anything, so resending is safe; a request that timed out is a different case — see Retrying a write.
Response envelope
Orders, deposits, and withdrawals return the resource object directly — and a bare array for lists. Newer resources — currencies, pending payments, banking accounts — wrap their result in an envelope of the form { "ok": true, "value": … }. The two shapes coexist, so follow the documented response for each endpoint in the reference rather than assuming one form everywhere.
{
"ok": true,
"value": [ /* … currencies … */ ]
}
Check each endpoint's documented response in the API reference to know whether it is enveloped or returns the object directly.