This guide walks through letting a user set a price alert on a market in your mobile app and pushing them a notification the moment the market crosses that price. It is built on the price_threshold webhook, with fire-and-delete so each alert fires once and cleans itself up.
How it fits together
- A user taps “Notify me at 75%” on a market in your app.
- Your backend creates a
price_threshold webhook that points at your server, scoped to that market, with one_shot: true.
- When the market’s price crosses the target, Struct delivers the event to your server.
- Your server looks up the device that registered the alert and sends a push notification.
- Because the webhook is
one_shot, it deletes itself after that single delivery. To repeat the alert, create a new one.
You will need a Struct API key, a server to receive the webhook, and a push provider for your app (this guide uses Expo, but FCM and APNs work the same way).
1. Create the alert when the user opts in
When the user sets an alert, create a price_threshold webhook from your backend and store the returned id against that user and device. Use min_probability for an “above” alert (fire when the price crosses up to the target) or max_probability for a “below” alert.
import { StructClient } from "@structbuild/sdk";
const struct = new StructClient({ apiKey: process.env.STRUCT_API_KEY! });
async function createPriceAlert(opts: {
conditionId: string;
target: number; // 0.0 - 1.0
direction: "above" | "below";
userId: string;
deviceToken: string;
}) {
const { data: webhook } = await struct.webhooks.create({
url: "https://api.yourapp.com/webhooks/struct",
event: "price_threshold",
secret: process.env.STRUCT_WEBHOOK_SECRET!,
filters: {
condition_ids: [opts.conditionId],
...(opts.direction === "above"
? { min_probability: opts.target }
: { max_probability: opts.target }),
one_shot: true,
},
});
// Map the webhook back to the device so the handler knows who to notify.
await db.priceAlerts.insert({
webhookId: webhook.id,
userId: opts.userId,
deviceToken: opts.deviceToken,
conditionId: opts.conditionId,
target: opts.target,
direction: opts.direction,
});
return webhook.id;
}
price_threshold requires condition_ids or position_ids when one_shot is set, which the per-market scope here already provides.
2. Receive the event and send the push
Struct delivers the price_threshold event to your endpoint as an HTTP POST. Verify the signature, look up the device that registered the alert, and forward a push notification.
import express from "express";
import type { WebhookEvent } from "@structbuild/sdk";
const app = express();
app.post(
"/webhooks/struct",
express.raw({ type: "application/json" }),
async (req, res) => {
// Verify X-Webhook-Signature before trusting the body.
// Full check: /webhooks/signature-verification
if (!verifyStructSignature(req)) return res.status(401).end();
const delivery: WebhookEvent = JSON.parse(req.body.toString("utf8"));
if (delivery.event !== "price_threshold") return res.status(200).end();
const { data } = delivery;
const alert = await db.priceAlerts.findByWebhookId(delivery.webhook_id);
if (!alert) return res.status(200).end();
await sendExpoPush(alert.deviceToken, {
title: data.direction === "up" ? "Price target hit" : "Price dropped",
body: `${data.question ?? "A market"} crossed ${Math.round(
data.threshold * 100,
)}% (now ${Math.round(data.price * 100)}%)`,
data: { conditionId: data.condition_id, marketSlug: data.market_slug },
});
// one_shot already removed the subscription on Struct's side; drop our row too.
await db.priceAlerts.deleteByWebhookId(delivery.webhook_id);
res.status(200).end();
},
);
async function sendExpoPush(
token: string,
payload: { title: string; body: string; data?: Record<string, unknown> },
) {
await fetch("https://exp.host/--/api/v2/push/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ to: token, sound: "default", ...payload }),
});
}
Always verify the
X-Webhook-Signature header before acting on a delivery. The full HMAC check is on the
Signature Verification page. Respond with a
2xx within 10 seconds so the delivery is not retried.
3. Let users cancel an alert
If a user removes a pending alert before it fires, delete the webhook so it never delivers:
await struct.webhooks.deleteWebhook({ webhookId: alert.webhookId });
await db.priceAlerts.deleteByWebhookId(alert.webhookId);
A one_shot alert that has already fired is gone on Struct’s side, so there is nothing to cancel once the push has been sent.
Recurring alerts
one_shot makes each alert fire once. For an alert that should keep notifying every time a market crosses a level (a “watchlist” toggle rather than a one-time target), omit one_shot: the subscription stays active and re-arms after each crossing. Delete it when the user turns the watch off.
Reading the payload
The fields you will use most in the notification:
direction: "up" (crossed the min_probability target) or "down" (crossed the max_probability target).
threshold: the target from your filter that was crossed.
price / probability: the value that triggered it, on a 0.0–1.0 scale.
question / market_slug / event_slug: for the notification copy and for deep-linking back into your app.
See the full field list on the price_threshold webhook page.