RPC WebSocket beta
Connect to Starkscan JSON-RPC subscriptions from a server with a header or from a browser with a URL token, once the WebSocket beta is enabled.
RPC WebSocket beta
WebSocket subscriptions on hosted Starkscan mainnet (SN_MAIN) are a
certified beta for enrolled clients. Every deploy keeps the lane on only if its
certification passes, and the separate 24-hour soak in the
readiness boundary passed on 2026-11-05. With the lane
off, an upgrade with a valid
batch-scoped key returns HTTP 503 with rpc_wss_disabled. Key checks run
first: a missing or unknown key gets 401, and a key without the batch scope
gets 403. Check
rpcProvider.websocketBeta.enabled in GET /v1/meta/capabilities before you
connect. The default Starkscan RPC contract is still authenticated HTTP
JSON-RPC; WebSocket support is a separate beta lane.
Endpoint and auth
There are two ways to connect. Both reach the same route with the same API key, the same methods, and the same limits.
From a server, send the key as a header:
wss://api.starkscan.co/v1/SN_MAIN/rpc/ws
X-Starkscan-Api-Key: <starkscan_api_key>From a browser, put the key in the URL. The browser WebSocket
constructor cannot set headers, so this is the only form a browser page can
use:
wss://starkscan.co/rpc/v0_10/SN_MAIN/<starkscan_api_key>It is the WebSocket twin of the HTTP nodeUrl
(https://starkscan.co/rpc/v0_10/SN_MAIN/<starkscan_api_key>). Use the
wss:// scheme with the same path. The edge sets the key from the path and
ignores any auth header or cookie the client sends. Like the HTTP nodeUrl,
the URL form works from a page on any origin: the key in the URL is its only
credential.
import { WebSocketChannel } from "starknet";
const channel = new WebSocketChannel({
nodeUrl: `wss://starkscan.co/rpc/v0_10/SN_MAIN/${apiKey}`,
});
await channel.waitForConnection();
const heads = await channel.subscribeNewHeads();
heads.on((head) => console.log(head.block_number));
// Later: await heads.unsubscribe();Treat the full URL as a secret, as you would the HTTP nodeUrl: it contains
your key. Anyone who reads it from your page source or network log can use it.
Use a key you can rotate, scoped for this use, and rotate it if the URL leaks.
Prefer the header form whenever your client can set headers.
The WebSocket route needs a key with the batch scope, for either form. A key
without it gets HTTP 403 at the upgrade, and an unknown key gets 401.
Enrollment request
Before testing WSS, ask your Starkscan partner or operator contact for named client WebSocket beta enrollment. Include:
- customer or workspace name
- chain and environment
- expected concurrent connections per API key
- expected subscriptions per connection
- subscription methods and filters you plan to use
- reconnect and HTTP backfill strategy
- which connection form you use: the header form from a server, or the URL token from a browser
Starkscan should return the approved key profile, connection and subscription caps, route, and current WSS certification boundary before you send production traffic.
Supported methods
The beta route serves every method of the Starknet v0.10.2 WebSocket API:
| Method | Use | Notification |
|---|---|---|
starknet_subscribeNewHeads | receive new-head notifications | starknet_subscriptionNewHeads |
starknet_subscribeEvents | receive event notifications for bounded filters | starknet_subscriptionEvents |
starknet_subscribeTransactionStatus | receive transaction status updates | starknet_subscriptionTransactionStatus |
starknet_subscribeNewTransactionReceipts | receive new transaction receipt notifications | starknet_subscriptionNewTransactionReceipts |
starknet_subscribeNewTransactions | receive new transaction notifications | starknet_subscriptionNewTransaction |
starknet_unsubscribe | close a subscription |
Params can be positional or named, as the spec allows. starknet.js sends
starknet_unsubscribe by name ({"subscription_id": "..."}), and both forms
release the subscription.
Notifications reach you exactly as the upstream node sends them. An active
subscription can also receive the server notification
starknet_subscriptionReorg. It is not a client subscription method. Treat
its affected block range as invalidated, roll back derived local state for that
range, and reconcile canonical blocks over HTTP before accepting later live
notifications.
Only the six request methods listed above can reach the WSS upstream. The
gateway rejects HTTP read, trace, write, unknown, and wildcard-looking method
names locally; a name that merely starts with starknet_subscribe is not
accepted. JSON-RPC request batches and binary WebSocket frames also fail closed.
Use authenticated HTTP RPC for every non-subscription request.
The capability manifest retains methodFamilies for older consumers, but that
field is classification-only. Integrations must use the exact methods list as
the forwarding contract; starknet_subscribe* is never a wildcard permission.
HTTP JSON-RPC still rejects subscription methods with the rpc_ws class. That
fail-closed behavior is intentional; use the WSS route for subscriptions and
the HTTP route for reads, calls, events backfill, simulation, fee, and
signed-payload submission.
Caps and backpressure
Beta caps are intentionally bounded. rpcProvider.websocketBeta in
GET /v1/meta/capabilities publishes the values the deployment enforces, even
while the lane is off:
| Limit | Capability field | Default |
|---|---|---|
| Concurrent connections per key | maxConnectionsPerKey | 2 |
| Pending or active subscriptions per connection | maxSubscriptionsPerConnection | 16 |
| Text frame size, either direction | maxMessageBytes | 1 MiB |
| Client JSON-RPC messages per key | maxClientMessagesPerMinute | 120 per minute |
| Upstream connect timeout | 5 seconds | |
| Client send timeout | 2 seconds | |
| Server ping to a silent connection | clientPingIntervalMs | 20 seconds |
| Idle close: no frame, not even a pong | clientIdleTimeoutMs | 60 seconds |
The two connection forms share one budget per key: two header connections and
one browser connection is three connections. Starkscan pings a connection that has been
silent for clientPingIntervalMs; browsers and standard WebSocket libraries
answer pings automatically. A connection that sends nothing, not even a pong,
for clientIdleTimeoutMs is closed, which gives its slot back to the key, so a
client that lost its network can reconnect within about a minute at the
defaults. A text frame over
maxMessageBytes closes the connection. Handle cap and rate-limit signals
explicitly:
| Signal | Meaning | Client action |
|---|---|---|
WebSocket upgrade returns HTTP 401 or 403 | the key is missing or unknown (401), or lacks the batch scope (403); a browser page using the header route instead of the URL form also gets 403 from its origin check | fix the key or its scope, and use the URL form from a browser; retrying does not help |
WebSocket upgrade returns HTTP 503 with code = "rpc_wss_disabled" | the key is valid, but the WebSocket lane is off on this deployment | do not retry in a loop; check rpcProvider.websocketBeta.enabled and keep your existing WSS provider |
WebSocket upgrade returns HTTP 429 | connection cap or WSS route budget is exhausted | honor Retry-After when present; otherwise back off with jitter before opening another connection |
JSON-RPC error error.data.code = "rpc_wss_subscription_limit" and error.data.quotaClass = "rpc_ws" | the connection hit its subscription cap | unsubscribe unused streams or open a new connection only if your key tier allows it |
JSON-RPC error error.data.code = "rate_limited" and quotaClass = "rpc_ws" | the key exhausted its per-message WSS budget | honor the returned retry delay before sending another request |
JSON-RPC error error.data.code = "rpc_wss_method_not_allowed" | the request is not one of the exact subscription methods above | move that call to authenticated HTTP RPC; do not retry it over WSS |
JSON-RPC error error.data.code = "rpc_wss_subscription_batch_unsupported" | subscription operations were sent as a JSON-RPC batch | send each subscription operation as one request with its own unique id |
HTTP JSON-RPC returns 429 with error.data.code = "rate_limited" for subscription probes | the HTTP fail-closed route is quota-limited while rejecting WSS-only methods | honor Retry-After; do not treat this as WSS subscription success |
| clean WebSocket close before a response | upstream or gateway backpressure, maintenance, or transient network loss | reconnect with exponential backoff, then backfill over HTTP before trusting live events again |
Every subscription request needs a unique JSON-RPC string or numeric id. A
subscription slot is released only after the upstream returns true for the
corresponding starknet_unsubscribe request. Treat a failed or interrupted
unsubscribe as still consuming the slot until the connection is closed.
Subscription operations must be sent one request at a time; a JSON-RPC batch
containing a subscription operation is rejected with
rpc_wss_subscription_batch_unsupported so the gateway can enforce the cap
without ambiguity.
Do not reconnect in a tight loop. Use exponential backoff with jitter and keep one reconnect attempt in flight per client instance.
Reconnect and backfill
WebSocket delivery is live, not an archival replay contract. Clients should keep their own cursor:
- latest observed block number for new heads
- event filter plus last processed block for event streams
- transaction hash for transaction-status subscriptions
On reconnect, resume by backfilling over HTTP before trusting live notifications again:
- use
starknet_blockNumberorstarknet_blockHashAndNumberto find the new head - use bounded
starknet_getEventswindows for event gaps - use
starknet_getTransactionReceiptorstarknet_getTransactionStatusfor transaction gaps
Apply the same HTTP reconciliation boundary after
starknet_subscriptionReorg; a reorg notification is an invalidation signal,
not a complete replacement-history payload.
Keep backfill windows bounded. For starknet_getEvents, prefer the named
filter wrapper, keep chunk_size <= 1024, and keep the block span at or below
10,000.
Error reports
For every WSS issue report, include:
- endpoint host and chain
- connection form: header, or browser URL token
- subscription method and params, with API keys removed
- close code and close reason, if present
- JSON-RPC error object, if present
- last notification block or cursor processed
- whether the client was reconnecting or opening a first connection
- matching HTTP backfill request IDs, if the issue involves a gap
Do not include the full API key, the URL-token RPC or WebSocket URL, private upstream URL, full event payload dumps, calldata, signatures, or raw subscription ids in shared tickets.
Readiness boundary
Do not treat WSS as ready because HTTP RPC provider readiness passed. A
customer-facing WSS claim needs two separate pieces of evidence. First, a
deploy attestation with rpcWssReadiness.status=passed, including Starkscan
WSS route proof for both the header and the browser URL-token forms, a
browser-mode connection that uses only the URL token, upstream WSS proof,
reconnect/backfill proof, and connection/subscription cap proof. Second, a
separate passed 24-hour new-heads soak report with no dropped heads when
checked against HTTP RPC. The deploy attestation does not run the soak; it
records it as skipped. The certification fixture also
proves that reorg notifications survive the gateway unchanged without
retaining their payload in the attestation.
Hosted mainnet has both. The 24-hour soak passed on 2026-11-05, against the
deploy of bb67f96b3:
- 51,195 new heads, none dropped while connected, and no regression without a reorg
- 4 reconnects, within the soak's limit of 20. The 591 blocks that fell in reconnect gaps were counted, not fetched: after a reconnect, a client must backfill them over HTTP
- a final lag of 0 blocks behind HTTP RPC
- 256 sampled block hashes, none mismatched and none unverified
The soak is a stability proof, not a per-deploy check. Every later deploy must
still pass rpcWssReadiness, or the sync turns the lane off.
rpcProvider.websocketBeta.status stays enabled_not_certified on a running
deployment: the server reports only that the route and upstream are
configured, and certification lives in the deploy attestation named by
certificationSource.
Related docs: