← tickerbot.io
View as markdown
Concepts

Webhooks

The table’s third tense. A webhook is a trigger plus a delivery: the clause you would scan, pushed to you when it becomes true, on the channel you choose.

Four doors, one object

POST /v2/webhooks is canonical; four /subscribe shortcuts build the identical webhook.

POST /v2/webhooks is the canonical create: pass a discriminated trigger object (scan, ticker, signal, or event) plus your delivery fields. Four shortcut endpoints build the identical webhook if a simpler body reads better: POST /v2/scan/subscribe, /v2/tickers/{ticker}/subscribe, /v2/signals/{signal}/subscribe, and /v2/events/subscribe. Whichever door you use, the webhook is listed and managed through the Webhooks endpoints: fetch and list, update the trigger or target, audit deliveries, re-enable after auto-suspension, and delete.

State-change deduplication

One event per not-matching → matching transition. Why you never receive 500 webhook events for one breakout.

For webhooks, the API tracks the last-known match state per (webhook, ticker) and only delivers an event when a ticker transitions from not-matching to matching. If a boolean flickers true → false → true inside a minute, you get one event. If a ticker stays in the match set for three days, you get one event when it first matched and nothing for those three days. To re-fire for that ticker, it has to leave the match set first.

Cadence sets the granularity. The match state is captured when the webhook is evaluated, and cadence controls how often that is: on realtime the flicker window is the ~1-minute refresh, but on hourly or nyse_open the window is the whole gap between evaluations — a ticker that enters and exits entirely between two evaluations never fires at all, and repeated flicker inside the window collapses to at most one event.

Polling is different

Deduplication is a webhook concept only.

Polling endpoints (/scan, /tickers, /signals) always return current state with no deduplication. If you’re polling and want edge-triggered behavior, diff the result set yourself between calls.

Delivery channels

One discriminator, set once when you subscribe. The same subscription can deliver to an HTTPS endpoint, a Discord channel, the Tickerbot mobile app, or stay in the dashboard.

  • webhook — POST a signed webhook.fired JSON payload to your target_url. The default when you pass a target_url.
  • discord — post a formatted embed to a Discord channel via an incoming-webhook URL. No HMAC — the URL is the credential.
  • in_app — record fires in the dashboard’s deliveries timeline only. No outbound request. The default when you pass neither a target_url nor a discord_url.
  • mobile_push — push a notification to a phone signed in to the Tickerbot mobile app. Sign into the app with your account and the device connects automatically; the app registers and rotates its device_id for you, so there’s nothing to call by hand. (The /v2/devices endpoints exist for that registration but are driven by the app, not something you integrate against.)
  • slack — reserved. Subscribing with this channel returns 501 not_implemented today.

If you omit channel, it’s inferred: webhook when target_url is set, discord when discord_url is set, otherwise in_app. The channel is fixed once the subscription exists — to switch, delete and re-subscribe.

Discord

Post embeds to a channel with two extra fields.

In Discord, open Server Settings → Integrations → Webhooks → New Webhook, pick a channel, and Copy Webhook URL. Pass it as discord_url with channel: "discord" on any subscribe endpoint:

curl -X POST "https://api.tickerbot.io/v2/scan/subscribe" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "above_sma_200 AND market_cap > 1e10",
    "channel": "discord",
    "discord_url": "https://discord.com/api/webhooks/123456789012345678/aBcDeF…",
    "cadence": "realtime"
  }'

Each fire posts a single embed: the subscription name, the composed query, and up to the first dozen matching tickers with price and day change, plus a “+N more” line. Discord embed limits apply — long match sets are truncated, not split.

The Discord URL is a credential

Treated like signing_secret — returned once, then masked.

Anyone with the incoming-webhook URL can post to your channel, so we store it as a posting credential and strip it from every read. The create response echoes it back once; GET /v2/webhooks/{id} and list omit it and set channel_config_present: true so tooling can still tell a channel is configured. Delivery rows mask it the same way. To rotate it, delete and re-subscribe with the new URL.

Retries & failures

Discord deliveries ride the same retry ladder, with one twist.

A 2xx (Discord returns 204) is a success. A 429 is retried, honoring Discord’s Retry-After. Other 4xx responses (e.g. a deleted webhook → 404) are permanent_failure — no retry. 5xx and network errors retry on the standard ladder (30s → 2m → 10m → 1h → 6h), and five consecutive failures auto-disable the subscription, exactly like an HTTPS webhook.

Validate it

Test fire works on every wire channel.

POST /v2/webhooks/{id}/test posts a real-shape fire to the configured channel right now — for Discord, a real embed with a [TEST] title prefix — and returns the inline outcome. It’s one-shot: failures don’t retry or count toward auto-disable.