Reference / Realtime API

Realtime API

Stream derived option analytics over a WebSocket, at cadences from one minute down to 100 milliseconds: forward, open interest, implied volatility, greeks and dealer exposures. Each channel carries one expiry of one root as a complete Arrow table. Quotes, trades and sizes are not sent.

Personal, non-commercial use only. API access is licensed for your own private use. You may not use it for a business or redistribute the data.

Before you connect

You need three things:

  1. A signed-in account. Credentials are issued to your session.
  2. API access. A qualifying plan lets you create your own key in your account settings. Accounts with an approved API data agreement can create one too. Keys are for personal, non-commercial use.
  3. A plan that includes the cadence you want. See Cadences.

Having an account or a credential does not grant a channel by itself. Each subscription is checked separately against your agreement and plan.

Create a credential

In the app, open Settings, then Data agreements, and create a key under API keys. Copy the secret straight away, because it is shown once. To create one from code instead, send this request with your session token as the bearer:

POST /api/data-agreements/v1/credentials
Authorization: Bearer <session token>
Content-Type: application/json

{ "delivery": "api" }

Response shape:

{
  "id": "6f1c2a9e-0000-4000-8000-000000000000",
  "secret": "cc_opra_<64 hex characters>",
  "expires_at": "2026-11-03T15:00:00Z"
}
RuleValue
API key lifetime1 year
Active credentials per account20
Secret visibilityReturned once, and can't be recovered

To revoke a credential, send DELETE /api/data-agreements/v1/credentials/{id}. A new credential is needed once the old one expires.

StatusMeaning
400The request body is invalid
401You aren't signed in
403Your account can't have this delivery, usually because the agreement is missing
409The request conflicts with your current state
503The service can't decide right now, so retry later

Error bodies are {"error": string, "code"?: string}.

Connect

The socket speaks the Centrifugo client protocol. Use its Protobuf transport, because the tables are binary and its JSON transport can't carry them.

The address is wss://stream.coreconvexity.ai/connection/websocket. This example uses the centrifuge JavaScript client. render is your own handler; apache-arrow reads the table.

import { Centrifuge } from 'centrifuge/build/protobuf';
import { tableFromIPC } from 'apache-arrow';

const SOCKET_URL = 'wss://stream.coreconvexity.ai/connection/websocket';
const client = new Centrifuge(SOCKET_URL, {
    data: new TextEncoder().encode(JSON.stringify({ credential: secret })),
});

const channel = client.newSubscription('derived:1s:SPY:2026-12-18', { delta: 'fossil' });
channel.on('publication', event => render(tableFromIPC(event.data)));
channel.on('error', event => console.error(event.error.code));
channel.subscribe();

client.connect();

Frames

Once your client has applied the delta, every publication is one complete Arrow table in IPC stream format: one record batch, one row per contract, ordered by strike and then right (call first). Read it with any Arrow library. A new subscriber gets the latest table when it subscribes, from recovery.

Subscribe with delta: 'fossil'. It's required: a subscription without it is refused with error 470, and retrying won't help. The first publication is a full table. After that the server sends only the bytes that changed and the client rebuilds the whole table. Rows keep their position, so unchanged contracts cost almost nothing.

The table always has every column. Fossil deltas are computed per channel, one stream of bytes shared by every subscriber, so a column selection can't be applied to it: pick the columns you want when you read the table. REST can narrow the columns with fields, below.

ColumnTypeNotes
expiryUtf8YYYY-MM-DD
rightUtf8C or P
strike_microsInt64Strike in millionths of a dollar
oiInt64Open interest
ivFloat32, nullableImplied volatility as a fraction
deltaFloat32, nullable
gammaFloat32, nullablePer $1
vannaFloat32, nullablePer vol point
charmFloat32, nullablePer calendar day
gexFloat32, nullableDollar gamma exposure per 1% move
dexFloat32, nullableDollar delta exposure
vexFloat32, nullableDollar vanna exposure per vol point
cexFloat32, nullableDollar charm exposure per day
midFloat32, nullableMid of the two-sided quote, in dollars. Null without a two-sided quote
premiumFloat64, nullablemid times open interest times 100, rounded to whole dollars. Float64 rather than Float32 because Float32 holds whole numbers exactly only up to 16,777,216, and one index contract's premium can exceed that. Null without a two-sided quote or open interest

A value that isn't available is a null, never NaN or a placeholder. The schema's metadata holds schema (derived-chain/v2), root, at_ns (the table's time in Unix nanoseconds) and, on the socket, tier. It also holds what belongs to the whole expiry rather than to a row, as decimal text, with the key absent when unknown:

KeyMeaning
forwardThe expiry's forward price
spotSpot implied from the forward (F·e^{-rT}), not a traded equity price

Quotes, sizes and trades are not in the table.

Our numbers can differ from other tools. See Why our numbers can differ.

Expiries

GET /api/market-data/v1/derived/expiries?root=SPY
Authorization: Bearer cc_opra_<64 hex characters>

The response lists the expiries you can build channels for, earliest first:

{ "root": "SPY", "expiries": ["2026-11-03", "2026-11-04"] }

Fetch a whole chain

REST requests go to https://coreconvexity.ai.

No subscription is needed for this request. It returns the latest one-minute chain as JSON, or as the same Arrow table the socket publishes:

GET /api/market-data/v1/derived/chain?root=SPY&expiry=*
Authorization: Bearer <API key>

expiry is * for every expiry, or one YYYY-MM-DD date.

fields is an optional comma-separated list of the columns you want, for example fields=oi,forward,gamma. It applies to JSON and Arrow alike. expiry, right and the strike always come back. The valid names are oi, forward, spot, iv, delta, gamma, vanna, charm, gex, dex, vex, cex, mid and premium; any other name is a 400 that lists them. Without fields you get everything. forward and spot are metadata in Arrow and top-level members in JSON, and with fields they are included only if you ask for them.

{
  "root": "SPY",
  "at_s": 1793740500,
  "forward": 751.32,
  "spot": 750.98,
  "contracts": [
    { "expiry": "2026-11-06", "right": "C", "strike": 750, "oi": 12034, "forward": 751.32, "spot": 750.98, "iv": 0.1432, "delta": 0.5512, "gamma": 0.0213, "vanna": 0.0041, "charm": -0.0123, "gex": 1250000, "dex": 4100000, "vex": 52000, "cex": -9800, "mid": 0.15, "premium": 180510 }
  ]
}

at_s is the minute described, in Unix seconds. Prices are in dollars and iv is a fraction. Gamma is per $1, vanna per vol point and charm per calendar day; gex, dex, vex and cex are dollar exposures and mid is in dollars and cents and premium is in whole dollars, as in Frames. For a single expiry forward and spot (implied from the forward) sit beside the contracts. A value that isn't available yet is null.

For large chains, send Accept: application/vnd.apache.arrow.stream to get the table described in Frames, with schema, root and at_ns in the metadata, and forward and spot too for a single expiry. Rows are ordered by expiry, strike and then right.

Channels

derived:{cadence}:{ROOT}:{YYYY-MM-DD}
PartExampleNotes
cadence1sOne of the cadences below
ROOTSPXWThe option root in capitals. An underlying with several roots, such as SPX and SPXW, has a channel per root
Date2026-12-18The expiry

Each channel is one expiry of one root, so subscribe once per expiry you want. Channels carry derived values only.

Cadences

CadencePublicationAccess
1mChanges every minuteIncluded with the plan
30sChanges every 30 seconds$25 add-on. Included for users who subscribed before the update
15sChanges every 15 seconds$50 add-on
1sChanges every second$100 add-on
100msChanges every 100 millisecondsLTF

A plan admits its own cadence and anything slower. Subscribing faster than your plan allows is refused for that channel only.

Staying connected

  • Refused access: access is rechecked while you're connected, and decisions last up to 30 seconds. Expect a revoked credential or ended plan to stop an open stream shortly after the change, not instantly.
  • Reconnecting: the channel recovers the latest table for you, so there is nothing to catch up on.
  • Connection limits: the limit applies across all your live connections, wherever they run.
  • Subscription limits: each API key can make 60 subscribe requests a minute, with a burst of 60 so you can open many expiries at once. Past that, a subscribe is refused with error 429 and the client retries by itself.
  • History: there's no history API yet. The socket delivers live data only.

© 2026

coreconvexity