CRISP partners

Keep accounts up to date

Automatic account refreshes with the browser SDK and cursor reads for backend consumers.

For progressive cash/availability reads, use the shared account-state store and forward your existing watchAccount notifications to it. Do not run two refresh stores or two realtime connections for the same account. That store publishes resources independently rather than waiting for a complete refresh batch.

The compatible createAccountSync helper below keeps cash, buying power, orders, positions and history up to date. Supply authenticated resource loaders; CRISP handles sessions, reconnects, event mapping, batching, cursor storage and resync. You do not need to implement the wire protocol below.

Account updates are invalidation signals, not balance deltas or market-price ticks. The helper re-reads authoritative data through your backend. It never places/retries trades, moves funds or blocks the order-submission path.

1. Expose an authenticated session route

Before you start: a ready account, realtime enabled, and your exact frontend origins saved in the Partner Dashboard under Trading → Realtime browser origins. Your backend must also have access to the resources it loads. Your backend authenticates the user, derives their external ID from its session, and calls the server SDK:

const result = await crisp.createRealtimeSession('example:user-123');
// Return result.data.session from your authenticated backend route.

HTTP examples omit signing headers. Substitute the authorized user's ID; never accept an unchecked browser ID. Return only result.data.session, with Cache-Control: private, no-store. The short-lived session may reach the authenticated browser. Your API credentials and execution capabilities must stay on the server.

2. Register resource loaders

These /api/my-* URLs are example routes on your app, not CRISP API routes. Each must authenticate the user. The session route returns the inner session object; resource routes return their SDK result.data.

import { createAccountSync } from '@crisp-trade/partner-sdk/realtime';

async function read(path, signal) {
  const response = await fetch(path, {
    signal, cache: 'no-store', credentials: 'same-origin',
  });
  if (!response.ok) throw new Error('Account data is unavailable');
  return response.json();
}

const account = createAccountSync({
  getSession: signal => read('/api/my-crisp-session', signal),
  loaders: {
    funding: signal => read('/api/my-account/funding', signal),
    orders: signal => read('/api/my-account/orders', signal),
    positions: signal => read('/api/my-account/positions', signal),
    trades: signal => read('/api/my-account/trades', signal),
  },
});
LoaderYour backend's SDK read
accountgetAccount
fundinggetFunding, using details: 'trading' for cash and buying power
orderslistOrders
positionslistPositions
tradeslistTrades, or listTradeFills for canonical fills
withdrawalslistWithdrawals
referralsgetReferralSummary

Register only resources used by the screen. Loaders own pagination and filters: return the page you display, not every historical row. Avoid getFunding with details: 'full' for background balances; that also loads deposit instructions. See cash and buying power.

Loaders should return data, not mutate UI state. Forward AbortSignal to fetch. The store discards obsolete results after stop/account switch and bounds reads with a 15-second default deadline. A callback ignoring abort can still perform its own side effects; the SDK cannot undo those.

3. Start once and render its snapshot

In non-React lifecycle code, start after login and stop on logout/unmount. syncAccount(options) returns the same store and starts immediately.

const unsubscribe = account.subscribe(() => render(account.getSnapshot()));
const stop = account.start();
render(account.getSnapshot());
// On logout/unmount: stop(); unsubscribe();

The snapshot contains data, connection, refreshing, stale, lastUpdatedAt and a safe error string. Data retains loader return shapes. With the routes above, cash is data.funding?.balance?.formatted; buying power is data.funding?.availableToTrade?.formatted.

Keep existing data visible during refresh and label stale/reconnecting states. Do not turn missing balances into zero. Successful HTTP reads may contain partial or null fields; handle those independently. lastUpdatedAt is a client refresh time, not proof that every upstream field is current.

React adapter

Use the external-store adapter below: no extra query library or React dependency in the SDK is required. Memoize options, recreating it when the signed-in user or resource filters change. identityKey is your app's session/account identity, not a CRISP credential or an ID sent to CRISP from the browser.

'use client';
import { useEffect, useMemo, useSyncExternalStore } from 'react';
import {
  createAccountSync, type AccountLoaders, type AccountSyncOptions,
} from '@crisp-trade/partner-sdk/realtime';

export function useCrispAccount<L extends AccountLoaders>(
  identityKey: string,
  options: AccountSyncOptions<L>,
) {
  const account = useMemo(
    () => createAccountSync(options),
    [identityKey, options],
  );
  useEffect(() => account.start(), [account]);
  const snapshot = useSyncExternalStore(
    account.subscribe, account.getSnapshot, account.getServerSnapshot,
  );
  return { ...snapshot, refresh: account.refresh };
}

Mount only inside your authenticated account UI. Changing the identity key selects an empty new store immediately; effect cleanup stops the old one. The store supports development effect cleanup/restart. Do not construct syncAccount() during rendering, because it starts network activity immediately.

For query caches, bind loaders to account-scoped query reads and return the actual refresh promise, not a fire-and-forget invalidation. Cursor commit must wait for completion. The SDK protects its store writes, not writes made independently by your cache library; keep cache keys scoped to the user.

What the SDK manages

  • Initial resource reads run even if the WebSocket is unavailable.
  • Reconnect/renewal reloads registered resources, then processes replayed notices.
  • Queued event bursts share the union of affected reads. Updates during a fetch receive a trailing refresh rather than being dropped.
  • Refresh rounds are serialized per store; loaders within a round run in parallel. Successful refreshes publish before their cursors are committed.
  • Failed event refreshes retain prior data/cursor, mark stale and reconnect/replay. Manual refresh() failures reject their promise so your UI can offer retry.
  • Default sessionStorage holds account-scoped cursors only, with memory fallback. storage: null selects memory-only storage. Tokens and balances are not persisted.
  • Stop cancels work and clears displayed data; it never cancels submitted trades.

Share one store across an account UI tree. Separate tabs have separate connections. There is no continuous polling fallback during a prolonged outage: offer account.refresh() for stale visible data. This does not guarantee upstream read-model freshness or replace recovery after an uncertain order.

EventResources refreshed when registered
order.updatedorders, funding
trade.recordedorders, positions, trades, funding, referrals
balance.updated, funding.updatedfunding
withdrawal.updatedwithdrawals, funding
position.redeemedpositions, funding, trades
settlement.updatedpositions, funding, referrals
account.status_changed, reconciliation.completed, unknown eventall registered resources

Browser realtime

Advanced transport: existing watchAccount(options) remains available for custom handling. Do not run it alongside createAccountSync for the same UI. See its callback reference.

Other clients connect to session.endpoint with WebSocket subprotocols [session.protocol, session.token], then send { type: 'resume', after: lastProcessedCursor || '0' }. Browser origins must match registration exactly. The browser cannot select an account topic.

Frames are ready, event (compact id, cursor, type, occurredAt) or resync_required (with cursor). Refresh applicable resources before committing an event cursor. On resync, refresh all resources before saving the supplied cursor; the gateway remains subscribed for subsequent events. Reconnect with backoff and renew sessions through your authenticated backend.

Cursor reads

Custom backend consumers can use listUpdates: GET /v1/accounts/{externalUserId}/updates?cursor=0&limit=100. Requires account-update access and a ready account. Returns events, compatibility alias updates, next cursor, hasMore; limit 1–200.

The HTTP envelope contains id, cursor, version: 1, type, externalUserId, accountId, occurredAt, data. WebSocket notices omit payloads. Persist only successfully processed cursors and deduplicate event IDs. Delivery is at least once; cursors can have gaps and timestamps can precede delivery. Confirm retention during onboarding; a gap requires full resource refresh, not assuming no activity.

Signed server webhooks

Webhooks are not part of the standard partner integration offering. Use createAccountSync() for application account updates, or the cursor API for backend consumers. If you need server-to-server automation, share the use case with CRISP for a separate enablement assessment. Do not assume webhook access or delivery, retry, retention or replay guarantees from account provisioning or the presence of an SDK verification helper.

The following protocol reference applies only if CRISP separately enables webhooks and agrees the operating arrangements for your integration. CRISP then configures an approved HTTPS endpoint and separate secret. Server SDK verifyWebhook({ rawBody, secret, timestamp, signature, eventId, cursor }) verifies exact bytes, timestamp skew and envelope/header identity.

X-Crisp-Event-Id: immutable event UUID
X-Crisp-Event-Cursor: event cursor
X-Crisp-Webhook-Timestamp: UTC timestamp
X-Crisp-Webhook-Signature: v1,{base64 HMAC-SHA-256}

Signed input is timestamp + '.' + exact_raw_body. Preserve raw bytes before JSON parsing. The helper verifies HMAC/identity, not durable business deduplication: record event IDs yourself, apply side effects idempotently, and return 2xx only after durable acceptance (including duplicates). The implementation follows no redirects and uses bounded retries before dead-letter handling. These mechanisms are not a delivery SLA; agree retry, retention, replay and support arrangements as part of separate enablement.

Never expose API or webhook secrets or raw account events to unauthenticated browsers. A realtime failure is never a reason to submit a replacement order.

CRISP Partner Documentation