RFQ API
The RFQ API gives programmatic access to request-for-quote trading on Silhouette: takers request quotes and accept them, makers price and fill them, and both sides manage balances, deposits, and withdrawals. It has two surfaces:
- The REST API for authentication, instruments, tokens, RFQs, quotes, balances, and funding. Every mutation goes over REST.
- The WebSocket API for live RFQ, quote, balance, and price streams.
Both share one authentication model, documented below. To see the whole flow end to end, follow your first trade. For a plain-language explanation of how RFQs work, see the RFQ overview.
Base URL
The production API host:
https://rfq-api.silhouette.exchange
REST endpoints live under /v1. The shared WebSocket client stream is wss://rfq-api.silhouette.exchange/v1/rfq/ws; makers publish price ladders to a separate maker-only socket at /v1/rfq/prices/ws.
Specifications
Both API surfaces are described by machine-readable specifications, which are the source for the interactive API Reference:
| Surface | Format | Spec | Reference |
|---|---|---|---|
| REST | OpenAPI 3.1 | /api/rfq-openapi.yaml | REST reference |
| WebSocket | AsyncAPI 3.0 | /api/rfq-asyncapi.yaml | WebSocket reference |
Authentication
The RFQ API uses wallet-based SIWE login to mint API credentials, then HMAC signatures on every private request. Public market-data endpoints are unauthenticated.
Login flow
- Request a single-use nonce with
POST /v1/auth/challenge. - Build a SIWE (EIP-4361) message that includes the nonce, gateway domain, URI, chain ID, and issued-at timestamp.
- Sign the SIWE message with the account wallet.
- Submit the SIWE message and wallet signature to
POST /v1/auth/api-keys. - Store the returned public
accessKeyand base64secret. - Sign each private request with the HMAC secret.
The secret is returned only once, at login, and never again. Treat it as a production secret and rotate it by minting a new key before revoking the old one.
Keep the secret out of the browser. The interactive API Reference sends public market-data requests only: login and the private operations are refused from that origin by design, since anything running on a page can read what you type into it. Sign requests from curl or your own client instead.
Whitespace and line breaks are part of the SIWE payload, so sign exactly the message you send to the API:
{domain} wants you to sign in with your Ethereum account:
{address}
Create Silhouette API key
URI: https://{domain}/
Version: 1
Chain ID: {chainId}
Nonce: {nonce}
Issued At: {issuedAt}
Signing requests
Every private request carries three headers:
| Header | Value |
|---|---|
Authorization | Bearer <access-key> |
Silhouette-API-Timestamp | Unix timestamp in milliseconds |
Silhouette-API-Signature | Base64 HMAC-SHA256 signature |
The signature is the HMAC-SHA256, under the secret, of the canonical string built from the exact request data the server receives:
{timestamp}
{METHOD}
{path}?{query}
{body}
For GET and bodyless DELETE requests the body line is empty. For query-string endpoints, such as DELETE /v1/auth/api-keys?all=true, include the query string in the signed path. Requests are accepted only when the timestamp is fresh within the gateway window, currently 30 seconds.
import base64
import hashlib
import hmac
import time
TIMESTAMP_HEADER = "Silhouette-API-Timestamp"
SIGNATURE_HEADER = "Silhouette-API-Signature"
def hmac_headers(access_key, secret_b64, method, path, body=""):
secret = base64.b64decode(secret_b64)
timestamp = str(int(time.time() * 1000))
message = "\n".join([timestamp, method.upper(), path, body]).encode()
signature = base64.b64encode(
hmac.new(secret, message, hashlib.sha256).digest()
).decode()
return {
"Authorization": f"Bearer {access_key}",
TIMESTAMP_HEADER: timestamp,
SIGNATURE_HEADER: signature,
}
For POST requests, sign the serialised JSON string you send. If the body bytes differ from the bytes used to compute the signature, the gateway rejects the request.
The shared WebSocket stream is authenticated the same way, in an in-band auth frame sent after connecting (the credential pair signs a canonical GET /v1/rfq/ws), because browsers cannot set headers on the WebSocket upgrade. The socket is authenticated once; later frames carry no signature. See the WebSocket API page for the frame protocol.
Key lifecycle
Multiple live keys can belong to the same account at the same time, which allows gradual rotation:
- Mint a new key.
- Move traffic to the new key.
- Confirm the new key can sign private requests.
- Revoke the old key with
DELETE /v1/auth/api-keys/{accessKey}.
For a sign-out-everywhere action, send DELETE /v1/auth/api-keys?all=true. The signing key used for that request is revoked too. The full set of auth endpoints is documented in the API Reference.
Rate limits
Authenticated traffic is limited per account. Traffic before authentication is limited per client IP.
| Scope | Limit |
|---|---|
Signed REST requests and frames on /v1/rfq/ws | 20 per second, burst 60, per account, one shared budget |
Ladders on /v1/rfq/prices/ws | 20 per second, burst 60, per account and instrument |
Login, WebSocket upgrades, and the auth frame | 100 per second, burst 200, per client IP |
Concurrent /v1/rfq/ws sessions | 8 per account; a maker holds one |
| Topic subscriptions | 256 per account, across all sessions |
/v1/rfq/prices/ws sockets | One per maker |
Not charged: ping and pong at 10 seconds apart or slower, and unsubscribe on every topic except openRfqs.
A refused REST request is a 429 with code RATE_LIMITED and a Retry-After header in whole seconds. A refused WebSocket frame is an error frame with code RATE_LIMITED, its message naming the wait; the socket and its subscriptions are unchanged. A refused ladder is dropped and the previous ladder stays live.