Open RadarPulse →
Developer guide · June 29, 2026

Options flow webhooks: automate alerts into Discord, Slack, and custom systems

A push notification on your phone tells you a sweep fired, but by the time you open the app, unlock it, navigate to the print, and decide to act, 60–120 seconds have passed. Webhooks cut that loop down to seconds. Here's how to wire options flow alerts directly into Discord, Slack, or any custom system.

The webhook model: why it beats polling

Options flow tools offer two consumption patterns:

For options flow specifically, webhooks are the right model. You want alerts delivered instantly and selectively (only high-score prints), not a continuous polling loop that makes you build rate-limit management and deduplication logic.

Webhook payload: what an options flow alert looks like

A standard options flow webhook payload contains everything you need to evaluate and act on a print:

{
  "event": "flow_alert",
  "timestamp": "2026-06-29T14:23:07.412Z",
  "ticker": "NVDA",
  "contract": {
    "type": "call",
    "strike": 130,
    "expiration": "2026-07-18",
    "dte": 19
  },
  "print": {
    "premium": 840000,
    "contracts": 200,
    "avgPrice": 4.20,
    "side": "ask",
    "classification": "sweep",
    "exchanges": ["CBOE", "PHLX", "BOX"]
  },
  "score": {
    "composite": 81,
    "volOiRatio": 7.4,
    "tier": "EXTREME"
  },
  "context": {
    "sector": "technology",
    "congressFlags": 1,
    "sectorConfluence": true
  },
  "links": {
    "flowDetail": "https://radarpulse.io/?t=NVDA_130C_0718",
    "chart": "https://radarpulse.io/?ticker=NVDA"
  }
}

Key fields to note: score.tier lets you filter on server side (EXTREME / ELEVATED / NOTABLE); context.congressFlags is non-zero if a Congress member has a recent STOCK Act disclosure on this ticker; context.sectorConfluence is true if 3+ same-sector names currently show elevated flow; print.classification distinguishes sweeps from blocks from complex legs.

Part 1: Discord bot (the simplest setup)

Discord webhooks require no bot token, no server-level permissions setup, and no hosting for basic delivery. Here's the complete flow:

Step 1: Create a Discord webhook URL

In your Discord server → channel settings → Integrations → Webhooks → New Webhook. Copy the URL; it looks like https://discord.com/api/webhooks/<id>/<token>.

Step 2: Register the webhook with your flow provider

Via the RadarPulse developer API (Elite tier):

curl -X POST https://radarpulse.io/api/dev/webhooks \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN",
    "events": ["flow_alert"],
    "filters": {
      "minScore": 70,
      "minPremium": 250000,
      "tiers": ["EXTREME", "ELEVATED"],
      "classifications": ["sweep"]
    },
    "secret": "your-hmac-secret-here"
  }'

Step 3: Discord receives the alert

Discord's webhook endpoint accepts a simple JSON body with content or embeds. The problem: RadarPulse sends the flow payload format, not the Discord message format. Two options:

Node.js middleware receiver (complete example)

// flow-to-discord.js — runs on Railway, Fly.io, or any Node host
const express = require('express');
const crypto = require('crypto');
const app = express();

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;

app.use(express.raw({ type: 'application/json' }));

function verifySignature(rawBody, signature) {
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');
  // Constant-time comparison prevents timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(signature.replace('sha256=', ''), 'hex')
  );
}

app.post('/webhook', async (req, res) => {
  const signature = req.headers['x-radarpulse-signature'];
  if (!signature || !verifySignature(req.body, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body.toString());
  if (event.event !== 'flow_alert') return res.sendStatus(200);

  const { ticker, contract, print, score, context, links } = event;
  const isCall = contract.type === 'call';
  const color = isCall ? 0x00c853 : 0xe53935; // green / red
  const premium = (print.premium / 1000000).toFixed(2);

  const embed = {
    color,
    title: `${score.tier} ${contract.type.toUpperCase()} — ${ticker}`,
    url: links.flowDetail,
    fields: [
      { name: 'Contract', value: `$${contract.strike} ${contract.type.toUpperCase()} exp ${contract.expiration} (${contract.dte}DTE)`, inline: false },
      { name: 'Premium', value: `$${premium}M`, inline: true },
      { name: 'Score', value: `${score.composite}/100`, inline: true },
      { name: 'Vol/OI', value: `${score.volOiRatio.toFixed(1)}x`, inline: true },
      { name: 'Classification', value: print.classification, inline: true },
      { name: 'Exchanges', value: print.exchanges.join(', '), inline: true },
      { name: 'Side', value: print.side === 'ask' ? 'At ask (buyer)' : 'At bid (seller)', inline: true },
    ],
    footer: {
      text: [
        context.congressFlags > 0 ? '🏛 Congress flag' : null,
        context.sectorConfluence ? '📡 Sector confluence' : null,
      ].filter(Boolean).join(' · ') || 'No context flags'
    },
    timestamp: event.timestamp,
  };

  await fetch(DISCORD_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ embeds: [embed] }),
  });

  res.sendStatus(200);
});

app.listen(process.env.PORT || 3000);
console.log('Flow-to-Discord receiver running');

This produces a Discord embed with color-coded direction (green calls, red puts), contract details, score, exchange attribution, and Congress/confluence flags in the footer.

Part 2: Slack integration

Slack's incoming webhooks use a similar pattern. The payload format differs (Slack uses blocks/attachments instead of Discord embeds), but the middleware pattern is the same:

// Inside the POST handler, replace the Discord fetch with:
const slackPayload = {
  attachments: [{
    color: isCall ? '#00c853' : '#e53935',
    title: `${score.tier} ${contract.type.toUpperCase()} — ${ticker}`,
    title_link: links.flowDetail,
    fields: [
      { title: 'Contract', value: `$${contract.strike} ${contract.type.toUpperCase()} ${contract.expiration} (${contract.dte}DTE)`, short: false },
      { title: 'Premium', value: `$${premium}M`, short: true },
      { title: 'Score', value: `${score.composite}/100`, short: true },
      { title: 'Classification', value: print.classification, short: true },
      { title: 'Vol/OI', value: `${score.volOiRatio.toFixed(1)}x`, short: true },
    ],
    footer: context.congressFlags > 0 ? 'Congress flag active' : 'No context flags',
    ts: Math.floor(new Date(event.timestamp).getTime() / 1000),
  }]
};

await fetch(process.env.SLACK_WEBHOOK_URL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(slackPayload),
});

Part 3: Zapier / Make (no-code automation)

For non-developers, Zapier and Make (formerly Integromat) can receive webhooks and forward the alert to any destination: email, SMS, Google Sheets, Notion, or dozens of other services. The setup:

  1. Create a Zap with "Webhooks by Zapier" as the trigger (Catch Hook). Copy the generated URL.
  2. Register that URL with your flow provider's webhook API with your desired filter settings.
  3. In Zapier, configure the action, e.g., "Create Google Sheets row" with the flow fields mapped to columns, or "Send email via Gmail" with the alert details formatted in the body.

Limitation: Zapier cannot verify HMAC signatures natively. If signature verification matters for your security posture, use a lightweight middleware host (Railway free tier, Fly.io, Render free tier) rather than pointing Zapier directly at the webhook endpoint.

See the full no-code automation guide: options flow alerts to Zapier/Make and EXTREME flow to Discord bot.

Webhook reliability: retries, idempotency, and error handling

A production-grade webhook receiver should handle three failure modes:

1. Duplicate delivery

Webhook systems retry failed deliveries. If your endpoint returns a 5xx, the provider will retry, and you might process the same alert twice. Use the event's timestamp + ticker + contract combination as a deduplication key:

const seen = new Set(); // use Redis in production

app.post('/webhook', async (req, res) => {
  // ... signature verification ...
  const event = JSON.parse(req.body.toString());
  const key = `${event.ticker}-${event.contract.strike}-${event.contract.expiration}-${event.timestamp}`;
  if (seen.has(key)) return res.sendStatus(200); // already processed
  seen.add(key);
  // ... process event ...
});

2. Slow receiver

Respond to the webhook provider immediately (within 5 seconds) and process the event asynchronously. If your Discord/Slack forward takes 2–3 seconds, acknowledge the webhook first:

app.post('/webhook', (req, res) => {
  res.sendStatus(200); // acknowledge immediately
  setImmediate(async () => {
    // ... process and forward to Discord/Slack ...
  });
});

3. Provider outage

If the RadarPulse API is unreachable, webhook events queue on the provider side and replay when the connection resumes. Ensure your receiver handles bursts (multiple events in quick succession) gracefully. Use a queue or rate limit your Discord/Slack calls to avoid rate-limit errors.

Advanced: programmatic signal processor

Beyond Discord/Slack, webhooks enable automated signal processing: logging every EXTREME print to a database for backtesting, triggering paper trades in a simulation account, or feeding a scoring dashboard. A minimal example that logs to SQLite:

const Database = require('better-sqlite3');
const db = new Database('flow-signals.db');

db.exec(`CREATE TABLE IF NOT EXISTS signals (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  received_at TEXT,
  ticker TEXT,
  contract_type TEXT,
  strike REAL,
  expiration TEXT,
  dte INTEGER,
  premium INTEGER,
  score INTEGER,
  tier TEXT,
  classification TEXT,
  vol_oi REAL,
  congress_flags INTEGER,
  sector_confluence INTEGER
)`);

const insertSignal = db.prepare(`
  INSERT INTO signals (received_at, ticker, contract_type, strike, expiration, dte,
    premium, score, tier, classification, vol_oi, congress_flags, sector_confluence)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

// Inside webhook handler:
insertSignal.run(
  new Date().toISOString(),
  event.ticker,
  event.contract.type,
  event.contract.strike,
  event.contract.expiration,
  event.contract.dte,
  event.print.premium,
  event.score.composite,
  event.score.tier,
  event.print.classification,
  event.score.volOiRatio,
  event.context.congressFlags,
  event.context.sectorConfluence ? 1 : 0
);

With a few weeks of signal data in SQLite, you can run the same backtesting analysis described in our options flow historical data guide, but on your own real-time-received alerts rather than historical exports.

Webhook vs. WebSocket: which to use

Webhook (HTTP push)WebSocket (persistent stream)
Use caseAlert delivery to Discord/Slack/custom endpointsBuilding your own real-time dashboard or processing every print
Connection modelStateless (each event is a separate HTTP call)Stateful (single persistent connection, continuous event stream)
Implementation complexityLow (standard HTTP receiver)Medium (requires reconnection logic, heartbeat handling)
ReliabilityAutomatic retries on failureManual reconnect on disconnect
FilteringServer-side, pre-deliveryClient-side, post-receipt
Best forAlert pipelines, logging, integrationsLow-latency dashboards, full tape processing

For most teams building alert workflows, webhooks are the right choice. Use WebSocket streams only when you genuinely need to process every print or build a custom real-time UI.

RadarPulse webhook API (Elite tier)

Elite-tier subscribers can register HMAC-signed webhooks via /api/dev/webhooks, filter by score tier, premium, classification, and ticker watchlist, and receive scored flow alerts in real time, including Congress flags and sector confluence signals.

Join the waitlist → or read the developer docs →

Frequently asked questions

What is an options flow webhook?

An options flow webhook is an outbound HTTP POST request from a flow data provider to a URL you control, fired each time a high-score options print arrives on the tape. Rather than polling an API or watching a dashboard manually, the webhook pushes the alert to your system (a Discord bot, a Slack channel, a custom logging endpoint, or an automated trading signal processor) within seconds of the print.

How do I build an options flow Discord bot?

The simplest path: (1) create a Discord webhook URL from your server's channel settings; (2) register that URL with your flow provider's webhook API; (3) configure a filter for minimum score (e.g., ≥70/100) and optionally a minimum premium ($500K+); (4) the provider POSTs each qualifying print to your Discord webhook URL, which Discord formats as a message in the channel. For richer embeds with color-coded calls/puts, use a lightweight middleware receiver (Node.js on Railway or Fly.io free tier) that transforms the payload into a Discord embed before forwarding.

How do I verify that an options flow webhook is authentic?

Reputable flow providers sign webhook payloads with HMAC-SHA256 using a shared secret you configure during registration. To verify: (1) extract the X-RadarPulse-Signature header; (2) compute HMAC-SHA256 of the raw request body using your webhook secret; (3) compare signatures using a constant-time comparison function. Reject any payload that fails this check; SSRF and replay attacks are possible without signature verification.

Can I filter options flow webhooks by ticker, score, or sector?

Yes. Most webhook implementations support server-side filtering. Common filter parameters: minimum composite score, minimum premium, direction (calls/puts/both), specific tickers (watchlist mode), specific sectors, and sweep-only flag. Server-side filtering is strongly preferable: it reduces bandwidth and processing load at your receiver, and ensures your endpoint only handles events you've pre-qualified.

What is the difference between an options flow webhook and a WebSocket stream?

A WebSocket maintains a persistent connection and delivers events continuously, best for high-frequency, low-latency consumption where you process every print. A webhook is event-driven and stateless: the provider calls your endpoint when a qualifying event occurs. Webhooks are simpler to implement, support automatic retries, and are better for alert delivery to external systems. Use WebSockets only when you need to process the full tape or build a custom real-time dashboard.