Macroplane › Blog › Stock Price Alert Webhooks: Signed Alerts for Your Bots and Workflows

Stock Price Alert Webhooks: Signed Alerts for Your Bots and Workflows

2026-10-28

Macroplane price alerts for any thesis or basket can now go to your own HTTPS endpoints as signed Standard Webhooks, with retries and delivery logs powered by the Webhook Relay Outbound Gateway.

More Macroplane supply-chain guides · Baskets · Investment theses · Macro trends

Stock Price Alert Webhooks: Signed Alerts for Your Bots and Workflows

Macroplane can now send price alerts for any thesis or basket to your own systems as signed webhooks. When a ticker you track moves past your threshold, Macroplane POSTs a JSON event to your HTTPS endpoint. That endpoint can be a trading bot, a Slack or Discord relay, an n8n or Zapier flow, or an internal dashboard. Every delivery is signed with the Standard Webhooks scheme, retried automatically if your server is down, and recorded in a delivery log you can inspect from Settings.

This post covers setup, the exact payload, how to verify signatures in Node.js and Python, and why we moved delivery onto the Webhook Relay Outbound Gateway instead of running it ourselves.

How it works

Price alerts already watch the companies in a thesis or basket and notify you when one moves more than a set percentage in a day. Until now the only reliable channel was email. Webhooks give you a second channel, and you can enable it alongside email or on its own.

Setup takes about two minutes:

  • Add an endpoint. Go to Settings → Webhooks and add the HTTPS URL that should receive events. Plain http:// URLs are rejected. You can add up to five endpoints, and each one can subscribe to every event or only to price_alert.triggered.
  • Copy the signing secret. When you create an endpoint, Macroplane shows its signing secret (whsec_…). Store it with the receiver. You can reveal it again later, or rotate it.
  • Enable the webhook channel on an alert. Open the price alert on a thesis or basket, for example the Memory Supercycle basket, and turn on Webhook. The threshold can be anywhere from 0.5% to 100%. The cooldown defaults to one day per ticker and can go as low as 30 minutes, so a volatile name does not flood your endpoint.
  • Send a test event. Settings has a Send test event button that delivers a webhook.test event to your endpoints. The alert dialog can also send a preview price_alert.triggered event built from today's actual moves, so you can test your parser against real data before you save the alert.

Each endpoint has a few controls of its own. You can pause it, and events are skipped until you resume (a stale price move is rarely worth replaying). You can rotate its secret. And you can open its delivery log, which lists each attempt with status, HTTP response code, retry count, duration and the first part of your server's response body. When a deploy on your side breaks the receiver, the log is the first place to look.

The price_alert.triggered payload

The request body is the event JSON itself, with no envelope. Here is an illustrative alert for the Memory Supercycle basket at a 5% threshold, with Micron ($MU) and Sandisk ($SNDK) both up more than 5% on the day:

Field notes:

  • entity_type is thesis or basket. entity_url links back to the page in Macroplane, which works well as a link in a Slack message.
  • tickers lists every mover in the sweep that triggered the alert. There is one event per alert, not one per ticker.
  • change_pct is the day's move against prev_close. is_positive gives the direction, and threshold_hit is the threshold that applied.
  • is_follow_up and since_last_pct show up when the same ticker already alerted earlier that day and has since moved another full threshold. Read since_last_pct in that case: a stock that spiked 15% and then pulled back to +5% is a pullback, not a new breakout.
  • next_earnings_date is only present when the company reports within the next 30 days. In the example, Sandisk reports the next day and Micron is further out.
  • test: true marks a preview sent from the alert dialog. Drop these before they reach anything that places orders.

The Settings test event is a different event type (webhook.test) with a much smaller body: {"message": "Test event from Macroplane", "sent_at": "…"}. Check for tickers before treating a body as an alert.

We treat this shape as a contract. New fields may be added, but existing ones will not be renamed or removed.

Verifying signatures

Every delivery carries three headers:

  • webhook-id: a unique message ID, which you can use for deduplication
  • webhook-timestamp: Unix seconds at signing time
  • webhook-signature: one or more space-separated v1,<signature> entries

The signature is a base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}. The key is your secret with the whsec_ prefix removed and the rest base64-decoded. For a broader explainer, see what a webhook signature is.

Three details are easy to get wrong:

  • Sign the raw bytes. If your framework parses the JSON and you re-serialize it, key order or whitespace can change and the signature will fail. Read the raw body.
  • Check the timestamp. Reject anything more than a few minutes old. Otherwise someone who captured a valid request could replay it later.
  • Accept any listed signature, compared in constant time. After you rotate a secret, deliveries are signed with both the old and new secrets for 24 hours, so the header carries two v1, entries. Match either one, and compare with a timing-safe function.

Node.js

Python (standard library only)

The verification uses only hmac, hashlib and base64; Flask is there just to receive the request. Because the scheme is Standard Webhooks, any library that implements the spec will also verify these deliveries.

Respond with a 2xx quickly and do any slow work, such as placing orders or calling other APIs, after you've responded. A slow or failing response counts as a failed delivery and will be retried. Since retries are possible, deduplicate on webhook-id before doing anything with side effects.

Why we didn't build delivery ourselves

Macroplane had webhook alerts before this release, but they were minimal. Our servers POSTed the JSON to a URL saved on the alert, with a 10-second timeout and three attempts spaced a few hundred milliseconds apart. There was no signature, so a receiver could not tell our request from anyone else's. If your server was down for the minute it took to deploy, the alert was gone. If it was down for a day, we kept trying it on every alert anyway. Neither of us could see what had happened.

A production webhook system needs more than that:

  • Signing, so receivers can authenticate requests and reject replays.
  • Durable retries with backoff that last hours, not seconds, and survive our own restarts.
  • Per-tenant isolation, so one user's endpoints, secrets and logs can never leak to another user.
  • Delivery logs that both sides can inspect.
  • Secret rotation that doesn't drop deliveries during the switch.
  • Auto-disable for endpoints that are clearly dead.
  • Idempotency, so a retried job on our side doesn't send the same alert twice.

We could have built all of this, but a queue, a retry scheduler, a signing service and a log store would be a lot of infrastructure for what is a secondary feature of an investment research product. So we moved delivery onto the Webhook Relay Outbound Gateway. The integration looks like this:

  • Each Macroplane user is an isolated consumer. The first time you add an endpoint, we create a Webhook Relay consumer tied to your Macroplane account, and your endpoints belong to it. Every endpoint operation (reveal, rotate, pause, test, read the log) first checks that the endpoint belongs to your consumer. A request for someone else's endpoint gets the same "not found" as a request for an endpoint that doesn't exist.
  • Our cron publishes one message. When the price-alert sweep fires, it publishes a single price_alert.triggered message addressed to your consumer. Webhook Relay delivers it to each of your endpoints that subscribes to that event. Publishing succeeds once Webhook Relay has durably accepted the message. Delivery happens asynchronously from then on, so a slow receiver never holds up the sweep for other users.
  • Idempotent publishes. Each real alert has a deterministic event ID built from the subscription, the minute it fired and a hash of the tickers involved. If our job retries after a partial failure, the second publish is deduplicated and you get one delivery, not two. Our end-to-end smoke test publishes the same alert twice and checks that exactly one request arrives.
  • Signing, retries and logs are handled by the gateway. Webhook Relay signs every delivery with the Standard Webhooks headers, retries failures automatically (up to 8 attempts over 48 hours), rate-limits per endpoint and keeps a per-attempt delivery history for 35 days. The delivery log in Settings reads from that history.
  • Rotation and auto-disable. Rotating a secret makes the gateway sign with both secrets for 24 hours. Endpoints are created with auto-disable on, so after about 12 hours of continuous failure Webhook Relay stops delivering to them. Resuming the endpoint re-enables it.

A side effect is that Macroplane no longer stores webhook URLs or signing secrets in its own database. HTTPS URLs saved on older alerts are imported as endpoints on your account, so existing integrations keep working. They also start receiving signatures, and you can begin verifying them whenever you're ready. Plain http:// URLs can't be imported. If you had one, add an HTTPS endpoint under Settings → Webhooks.

The tradeoff is a dependency: if the gateway is unavailable, publishing fails. The alert then counts as not delivered and is retried on the next sweep, not silently dropped. If you're building webhooks into your own product and weighing the same decision, the outbound docs cover the consumer, endpoint and message model, and the gateway is available on every Webhook Relay plan, including Free.

Which events can I subscribe to?

Two today: price_alert.triggered, which fires when tickers on a thesis or basket cross your threshold, and webhook.test, which is sent from Settings to check an endpoint. An endpoint subscribed to all events will also receive any event types we add later.

Can I send price alerts to Slack or Discord?

Yes, through a small relay: a serverless function, an n8n or Zapier webhook trigger, or your own bot. It verifies the signature, formats tickers into a message, and posts it to the channel's incoming webhook. Slack and Discord expect their own message formats, so pointing the endpoint directly at a Slack or Discord URL won't render correctly.

What happens if my server is down when an alert fires?

The delivery is retried automatically with backoff, up to 8 attempts over 48 hours, and every attempt shows up in the endpoint's delivery log. If you would rather not receive alerts at all during planned downtime, pause the endpoint: events are skipped while it is paused and the alerts that fire after you resume are delivered as usual.

How do I avoid processing the same alert twice?

Store each webhook-id you've processed and skip repeats. On our side, real alerts are published with deterministic IDs, so a retried sweep doesn't create a second message.

Is this financial advice?

No. Price alerts report observed market moves. What your systems do with them is up to you, and anything that trades automatically should be tested with test: true previews and safeguards of your own before it handles real orders.

Related reading

  • Connect Claude.ai to Macroplane via MCP for Automated Research: the other way to automate Macroplane, through natural language instead of events
  • Micron Stock ($MU): The Record Quarter, the Memory Supercycle, and Why It Fell Anyway: the company behind the $MU alerts in the example above

Which events can I subscribe to?

Two today: price_alert.triggered, which fires when tickers on a thesis or basket cross your threshold, and webhook.test, which is sent from Settings to check an endpoint. An endpoint subscribed to all events will also receive any event types we add later.

Can I send price alerts to Slack or Discord?

Yes, through a small relay: a serverless function, an n8n or Zapier webhook trigger, or your own bot. It verifies the signature, formats tickers into a message, and posts it to the channel's incoming webhook. Slack and Discord expect their own message formats, so pointing the endpoint directly at a Slack or Discord URL won't render correctly.

What happens if my server is down when an alert fires?

The delivery is retried automatically with backoff, up to 8 attempts over 48 hours, and every attempt shows up in the endpoint's delivery log. If you would rather not receive alerts at all during planned downtime, pause the endpoint: events are skipped while it is paused and the alerts that fire after you resume are delivered as usual.

How do I avoid processing the same alert twice?

Store each webhook-id you've processed and skip repeats. On our side, real alerts are published with deterministic IDs, so a retried sweep doesn't create a second message.

Is this financial advice?

No. Price alerts report observed market moves. What your systems do with them is up to you, and anything that trades automatically should be tested with test: true previews and safeguards of your own before it handles real orders.

Referenced on this page

  • Memory Supercycle basket
  • Connect Claude.ai to Macroplane via MCP for Automated Research
  • Micron Stock ($MU): The Record Quarter, the Memory Supercycle, and Why It Fell Anyway