Documentation
Advanced

Best practices

Habits that keep your Deriv API integration robust, efficient, and resilient. Apply these alongside the per-endpoint reference.

Connection management

Open a single WebSocket connection and reuse it for every request and subscription, instead of opening one connection per call. Multiplex requests over the shared socket and match each response to its request using req_id.

When the connection drops, reconnect with exponential backoff (for example 1s, 2s, 4s, 8s, capped) plus a little jitter, rather than retrying in a tight loop. Send a periodic ping to keep the connection alive and to detect silent disconnects early.

For an authenticated connection, request a fresh OTP URL before each reconnect. Each URL works only once.

// keepalive: send a ping on an interval over the shared socket
setInterval(() => socket.send(JSON.stringify({ ping: 1 })), 30000);

Rate limiting and request throttling

Throttle outgoing requests to stay within the API's rate limits. Queue and pace calls instead of sending large bursts, and debounce user-driven actions so a single interaction doesn't fan out into many requests.

If you're rate limited, back off exponentially before retrying, increasing the delay after each rejection. Prefer subscriptions over polling for data that updates continuously, so the server pushes changes instead of you re-requesting them.

You can reconcile payment agent transfers and withdrawals by request ID. Send a unique request_id with each one and, if the response is lost, check its result with Transfer status or Withdrawal status instead of sending it again. A purchase has no request ID lookup: req_id only matches a response to its request. To avoid a duplicate purchase, buy by proposal ID: the same proposal ID can't be bought twice.

For the figures themselves — the per-IP and per-account request limits, and the shared budgets that groups of WebSocket calls draw on — see Limits.

Authentication and token hygiene

Request an authenticated WebSocket URL with POST /trading/v1/options/accounts/{accountId}/otp. Send your token in the Authorization: Bearer header. Read the options WebSocket documentation for details.

Add the Deriv-App-ID header when you use a Personal Access Token (PAT). OAuth 2.0 access tokens don't need this header. Read the authentication guide for details.

Connect directly to the URL in data.url. The URL contains a one-time password (OTP), so you don't need to send an authentication message after connecting.

The one-time password is valid for 120 seconds and works only once. Request a fresh OTP URL before every reconnect.

Use tokens with the least privilege the task requires by requesting only the scopes your endpoints need, such as trade or account_manage; a token that lacks a required scope is rejected with a 403 error.

For user-facing applications, use the OAuth 2.0 Authorization Code flow with PKCE rather than embedding tokens. Always perform the token exchange on your backend, not in the browser, and validate the state parameter on the callback before using the authorization code. Authorization codes are single-use and short-lived, so exchange them immediately and never store or log them. Generate a fresh code_verifier and state for every request, and clear them once the exchange succeeds. Access tokens expire after the number of seconds in expires_in. Deriv doesn't issue refresh tokens, so run the authorization flow again when a token expires or a call returns a 401 error. If a new access token also gets a 401, don't start the flow again. Check the Authorization header and the endpoint instead. See the OAuth 2.0 guide for the full implementation.

Never embed long-lived API tokens in client-side code, public repositories, or build artifacts. Store secrets server-side, rotate them regularly, and revoke any token that may have been exposed.

// Run this on your server, or in the browser only with a short-lived OAuth 2.0 access token.
// Never ship a Personal Access Token (PAT) in client-side code.
async function connectAuthenticated(accountId, token) {
  const response = await fetch(
    `https://api.derivws.com/trading/v1/options/accounts/${accountId}/otp`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        // Add this header for PAT authentication only:
        // "Deriv-App-ID": "<app-id>",
      },
    }
  );

  if (!response.ok) {
    throw new Error(`OTP request failed (${response.status})`);
  }

  const result = await response.json();
  const url = result?.data?.url;
  if (!url) {
    throw new Error("OTP response did not include a WebSocket URL");
  }

  return new WebSocket(url);
}

connectAuthenticated("<account-id>", "<token>");
// Request a fresh OTP URL before every reconnect.

Error handling and subscription cleanup

Always inspect the error field on every response before using its data, and surface the error code and message to your own logging. Match responses to requests using req_id so a failure is attributed to the correct call.

Release subscriptions you no longer need: use forget with a subscription id to stop a single stream, and forget_all by stream type to stop every stream of that kind. Cleaning up prevents leaked subscriptions and unnecessary traffic.

// stop a single stream by its subscription id
socket.send(JSON.stringify({ forget: subscriptionId }));
// stop every stream of a given type
socket.send(JSON.stringify({ forget_all: "ticks" }));

Contract lifecycle

A trade moves through a short lifecycle: request a price with proposal, execute it with buy, follow the open position with proposal_open_contract, and close it with sell or let it settle at expiry. Buy against a fresh proposal rather than a stale quote — prices are short-lived. Pass a maximum price in the buy call so moves between quote and execution don't leave you with a worse fill.

Track each open contract by subscribing to proposal_open_contract instead of polling portfolio. The subscription pushes spot and profit updates and flips the is_sold and is_expired flags the moment the contract settles.

When is_sold becomes true, call forget on that subscription so you release the stream and don't leak subscriptions over time.

A buy response with an error object means no contract was created. If the response is lost or times out, check statement before you send the purchase again. Request action_type buy, a date_from just before you sent it, and description set to 1. Match the row on its app ID, amount, shortcode and transaction time, then follow its contract_id with proposal_open_contract.

statement lists every executed buy as soon as it's committed, so no matching row means no contract was bought. req_id only matches a response to its request. A proposal ID can't be bought twice, but a buy with parameters isn't deduplicated, so sending buy: 1 again can open a second contract. That's why buying by proposal ID is the safer choice.

Before closing early, check the contract's own flags — use sell only when is_valid_to_sell is true, and use cancel only within the deal cancellation window for supported trades. For supported contract types, set stop loss and take profit with contract_update so exits happen server-side rather than watching the stream and selling manually.

// track an open contract, then release the stream once it settles
socket.send(JSON.stringify({
  proposal_open_contract: 1,
  contract_id: contractId,
  subscribe: 1,
}));

// on each update, when data.proposal_open_contract.is_sold is true:
//   socket.send(JSON.stringify({ forget: subscriptionId }));

Pagination

For endpoints that return lists, request data in pages rather than fetching everything at once. Use the limit and offset parameters to walk through results, and keep page sizes modest to reduce latency and memory pressure.

Continue requesting subsequent pages until a page returns fewer rows than the limit you asked for, which signals that you've reached the end of the data set.

Any other questions? Get in touch

Click to open live chat support. Get instant help from our support team.