ThetaFlow
  • Features
  • Strategies
  • How it works
  • Pricing
  • Sign in

Webhooks

Payload version 1 · Last updated: September 18, 2026

ThetaFlow can post every notification to a URL of yours as plain JSON: fills, orders, opened and closed positions, rolls, settlements, errors, auto-pauses, and P&L reports. There is no Discord, Telegram or Slack formatting baked in. Point it at your own glue (n8n, Zapier, Make, a Cloudflare Worker, a few lines of Python), filter on type and data.strategy, and reshape it however you like.

Set the URL under Settings → Webhook, press Send test to post a sample and see your endpoint's answer, then save. Every kind is posted whatever the notification toggles on that page say — those govern email and push only — so your receiver decides what matters by filtering on type. Clear the field to turn the webhook off.

How delivery works

  • One POST per event, body application/json, UTF-8.
  • Headers: X-ThetaFlow-Event carries the event type and X-ThetaFlow-Delivery the event id. The user agent is ThetaFlow-Webhook/1.
  • Answer with any 2xx status within 10 seconds. The response body is ignored. Redirects are not followed.
  • A timeout, a connection error, a 5xx, a 408 or a 429 is retried with growing backoff, up to 8 attempts over roughly an hour and a quarter. A retry carries the same event id, so if you record ids you can drop duplicates. A redirect or any other 4xx is treated as a configuration problem and not retried. The settings page shows the outcome of the latest attempt.
  • Events go out moments after the notification is raised. Fills and closed positions follow ThetaFlow's fill sync, normally within a minute of the trade.

Security

Requests are not signed. Like a Discord or Telegram webhook, the URL itself is the secret: use a long random path, keep it out of screenshots and shared configs, and rotate it by saving a new one. Any https:// or http:// URL is accepted, including one on your own network.

The envelope

Every event is wrapped the same way, so one handler can route on type:

{
  "id": "evt_5f1c9b2a7d3e4c6b8a9f0e1d",
  "type": "position-closed",
  "version": 1,
  "created_at": "2026-09-18T14:02:41Z",
  "data": { "strategy": "spy-em-breakout", "strategy_name": "SPY EM breakout", "underlying": "SPY", "realized": "+37.72", ... }
}
FieldMeaning
idStable id for this event. A retry of the same event keeps its id; a new event, even with identical data, gets a new one.
typeWhich notification this is; one of the types below. Filter on it: every kind is posted.
versionPayload schema version, currently 1. New fields may be added without bumping it; a renamed or removed field would.
created_atWhen ThetaFlow queued the event, RFC 3339, UTC.
testPresent and true only on the sample the Send test button posts. Absent on real events.
dataThe event body; its shape depends on type.

Across every type, data.strategy is the strategy's slug (the identifier in its URL and order tags), "" when the event isn't tied to one strategy, such as a manual order, and data.strategy_name is its display name as shown in the app, resolved when the event is sent ("" when the slug is unknown). Money and size fields are strings so nothing is lost to floating point; quantity is a plain integer.

Event types

stock-purchase — a trade filled

A strategy's order executed. One event per execution, opening and closing alike: a two-leg spread is two events, a leg that fills in pieces is several, and a round trip is at least two more. action is the broker's side (Buy to Open, Sell to Open, Buy to Close, Sell to Close): filter on it to tell entries from exits. Fills that belong to a roll are folded into roll-executed and do not fire this event. The name is historical; the event covers every fill, not just purchases.

"data": {
  "strategy": "spy-em-breakout",
  "strategy_name": "SPY EM breakout",
  "underlying": "SPY",
  "symbol": "SPY   260918C00650000",
  "action": "Buy to Open",
  "quantity": 2,
  "price": "0.55",
  "account": "5WX12345"
}

order-placed — an order submitted

The moment a strategy (or you, from the app) submits an order. summary is the ticket in words.

"data": {
  "strategy": "spy-em-breakout",
  "strategy_name": "SPY EM breakout",
  "summary": "Buy to Open 2× SPY $650C 0DTE @ 0.55"
}

position-opened — a new position exists

Sent once per position, after its entry's first fill lands. legs lists the fills known at that moment, folded per symbol and action, and net is their dollars after fees (on Alpaca, before its pass-through exchange fees, which it charges as one daily total), signed like realized: negative for a debit you paid, positive for a credit you collected. Both are a snapshot: a leg whose fill hasn't posted yet isn't included and isn't revisited, and a position that opened and closed within one sync includes its exit, so net then reads as the round trip; position_id is the same value the matching position-closed carries. Each event stands on its own, so you don't have to keep one to render the other; the id is there if you want to correlate them, and a position opened and closed within one sync can deliver its two events in either order.

"data": {
  "strategy": "spy-em-breakout",
  "strategy_name": "SPY EM breakout",
  "underlying": "SPY",
  "account": "5WX12345",
  "position_id": "7Kd2pQ9m",
  "net": "-111.30",
  "legs": [
    { "symbol": "SPY   260918C00650000", "action": "Buy to Open", "quantity": "2", "price": "0.55" }
  ]
}

position-closed — a position went flat

Sent once per position, when its fills net flat. It describes the whole round trip on its own, so you never need to have kept the position-opened event: legs holds every fill, opening and closing, folded per symbol and action; opened_at is the first fill's execution time and closed_at when ThetaFlow marked the position closed; realized is the net dollars after fees (on Alpaca, before its daily pass-through exchange fees), signed.

"data": {
  "strategy": "spy-em-breakout",
  "strategy_name": "SPY EM breakout",
  "underlying": "SPY",
  "account": "5WX12345",
  "position_id": "7Kd2pQ9m",
  "realized": "+37.72",
  "opened_at": "2026-09-18T13:31:02Z",
  "closed_at": "2026-09-18T14:02:11Z",
  "legs": [
    { "symbol": "SPY   260918C00650000", "action": "Buy to Open", "quantity": "2", "price": "0.55" },
    { "symbol": "SPY   260918C00650000", "action": "Sell to Close", "quantity": "2", "price": "0.74" }
  ]
}

settlement — options expired or assigned

event is the human label, e.g. expired or assigned.

"data": {
  "strategy": "spy-covered-call",
  "strategy_name": "SPY covered call",
  "underlying": "SPY",
  "symbol": "SPY   260918C00650000",
  "quantity": 1,
  "event": "expired",
  "account": "5WX12345"
}

roll-executed — an option rolled

One event per roll. Its closing and opening fills do not also fire stock-purchase.

"data": {
  "strategy": "spy-tail-hedge",
  "strategy_name": "SPY tail hedge",
  "underlying": "SPY",
  "summary": "Rolled tail hedge (held put at 5 DTE), bought 2× SPY $560P Nov 20 @ 3.10",
  "account": "5WX12345"
}

error — a strategy run failed

"data": {
  "strategy": "spy-em-breakout",
  "strategy_name": "SPY EM breakout",
  "message": "couldn't fill the entry within the 09:30–09:45 window"
}

A liquidation that left positions open adds "still_open": true; its message names each leg still open. It is not retried — close them at the broker. Each such failure is its own event, with its own id. A failure that also paused the strategy adds "paused": true: it isn't retried until the strategy is resumed. A failure of an order you placed yourself from the app (a roll from the strategy's page) adds "hand_placed": true: it isn't retried, and it doesn't count toward pausing the strategy.

strategy-auto-paused — automation paused a strategy

"data": {
  "strategy": "spy-em-breakout",
  "strategy_name": "SPY EM breakout",
  "reason": "It failed 3 times in a row. Last error: couldn't fill the entry within the 09:30–09:45 window."
}

pnl-report — a performance summary

Weekly, monthly, and annual. overall and each line's realized are formatted, signed dollars, and lines carry the strategy's display name rather than its slug. A report that covers two periods at once (the annual alongside the monthly) carries the second under also with the same shape. app_url is the app's origin, for building links.

"data": {
  "cadence": "Weekly",
  "period": "Sep 8 – Sep 12, 2026",
  "overall": "+$412.30",
  "trades": 9,
  "win_rate": "67%",
  "empty": false,
  "lines": [
    { "name": "SPY EM breakout", "realized": "+$287.10", "trades": 5 },
    { "name": "QQQ scalp", "realized": "+$125.20", "trades": 4 }
  ],
  "app_url": "https://app.thetaflow.app"
}

welcome, brokerage-reset — account messages

Account messages, which email always sends too. welcome carries name; brokerage-reset carries name, reconnect_url and broker (the connection, e.g. Alpaca · Paper; absent for Tastytrade) and means that brokerage connection needs re-linking.

A minimal receiver

Anything that answers 200 works. For a Discord channel, for example:

// Cloudflare Worker: forward closed positions to a Discord webhook
export default {
  async fetch(request) {
    const ev = await request.json();
    if (ev.type === "position-closed" && !ev.test) {
      const d = ev.data;
      await fetch(DISCORD_WEBHOOK_URL, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ content: `${d.strategy} closed ${d.underlying}: ${d.realized}` }),
      });
    }
    return new Response("ok");
  },
};
ThetaFlow
Automated options strategies on your Tastytrade or Alpaca account.
Features Pricing Log in

ThetaFlow is a software tool, not a broker or financial advisor. Options trading involves substantial risk and is not suitable for everyone. Nothing here is financial advice.

Terms Privacy Refunds Webhooks Logos provided by Logo.dev
© 2026 ThetaFlow. All rights reserved.