View as markdown
Endpoints · Webhooks

Create a webhook

POSThttps://api.tickerbot.io/v2/webhooks

Canonical create: a webhook is a trigger plus a delivery.

objectrequired

What fires the webhook. A discriminated object — trigger.type picks the shape, and the fields below belong inside it. Each shape is also available as a flat-params shortcut: POST /v2/scan/subscribe, /v2/tickers/{t}/subscribe, /v2/signals/{s}/subscribe, /v2/events/subscribe.

enumrequired

Which trigger shape the rest of the object uses.

string

**scan**: required — the SQL WHERE any ticker must match to fire. **ticker**: required — WHERE fragment evaluated for that ticker (auto-scoped; don't add ticker = … yourself; trigger.condition accepted as an alias). **event**: optional row-STATE filter on the event's ticker at fire time (market_cap > 1e10).

string

**signal**: required — a built-in signal name (e.g. rsi_14) or one of your custom signals (custom SQL is expanded and frozen at creation).

string

**signal**: required for numeric signals — a single bound like >70; sending one with a boolean or custom signal returns 400 (it does not apply). **ticker**: accepted as the original alias of trigger.q.

string

**ticker**: required — the symbol to watch (e.g. NVDA). **signal**: optional — restrict the signal to one symbol (omit to watch the whole universe).

string

**event**: optional symbol list, max 50 (e.g. AAPL,NVDA). Mutually exclusive with trigger.universe.

string[]

**event**: required — event kinds to fire on, array or comma list (e.g. split,analyst).

string

**event**: optional event-CONTENT filter in the /v2/events grammar over (ticker, ts, kind, payload) — e.g. payload->>'firm' = 'Goldman Sachs'. Composes with trigger.q.

string

**scan / signal / event**: optional universe slug (top_10, top_100, or one of yours) scoping which tickers can fire. Mutually exclusive with trigger.tickers on event.

string

HTTPS delivery URL (the webhook channel), max 1024 characters. Omit for in-app delivery, or use channel + discord_url/device_id for other channels.

enum

Delivery channel. See Delivery channels.

webhookPOST the payload to an HTTPS endpoint you own, signed with X-Tickerbot-Signature.
in_appDeliver to the in-app feed. No external receiver, so nothing to validate and no test_url.
discordPost to a Discord channel via its webhook URL. Delivered as a Discord embed, never HMAC-signed — the URL is itself the credential.
mobile_pushPush to a registered device. Requires a device_id from Register a device.
string

Discord webhook URL (channel discord).

string

Registered device id (channel mobile_push, see /v2/devices).

enumdefault realtime

Evaluation cadence — a user preference — never gated. Event triggers deliver on ingest — only realtime is accepted on them (400 otherwise).

realtimedefaultEvaluate on every refresh — the canonical value. (1m is a deprecated alias that collapses to this.)
hourlyBatch schedule: evaluate once an hour.
nyse_openBatch schedule: evaluate once per session, at the NYSE open.
string

Display name, max 80 characters. Defaults to an auto-generated one from the trigger.

string

Extra columns echoed in fired payloads' match rows (fields accepted as an alias). Not accepted on event triggers (400) — event deliveries carry the event payload, not state rows.

stringdefault market_cap

Column the fired payload's match lists are sorted by before the 100-row cap is applied — so a truncated list is the deterministic top 100, not an arbitrary sample. Same contract as POST /v2/scan. Not accepted on event triggers (they deliver one event at a time).

enumdefault desc

Sort direction for order. Not accepted on event triggers (400).

ascAscending — smallest or earliest first.
descdefaultDescending — largest or most recent first.
string

Optional unique string (≤255 chars). A retry carrying the same key within 24h replays the original response (Idempotency-Replayed: true header) instead of creating a duplicate; a concurrent duplicate gets 409 idempotency_in_flight; reusing a key for a *different* request (another method or path) gets 422 idempotency_key_reused. 5xx responses are not stored — those retries re-execute.

201
The created webhook record (same shape as GET /v2/webhooks/{id}) + test_url.
400
Missing/invalid trigger, bad condition, or invalid delivery fields.
403
webhook_tier_required (Free plan) or webhook_limit_reached (at your plan's cap).
404
Trigger references an untracked ticker, unknown signal, or missing universe; or device_not_found — the device_id isn't registered to your account.
409
idempotency_in_flight — a concurrent request with the same Idempotency-Key is still executing; retry after it settles.
422
idempotency_key_reused — the Idempotency-Key was already used for a *different* request (another method or path). Use a fresh key per distinct request.
501
channel_not_implementedchannel: "slack" is declared but not yet deliverable.
  • Four trigger types. `scan`{ "type": "scan", "q": "…", "universe"?: "slug" } fires on any ticker matching a SQL WHERE. `ticker`{ "type": "ticker", "ticker": "NVDA", "condition": "…" } fires when one ticker matches, auto-scoped, so do not add ticker = … yourself.
  • `signal`{ "type": "signal", "signal": "rsi_14", "condition"?: ">70", "ticker"?: "NVDA", "universe"?: "slug" } fires on a named signal: booleans need no condition, numerics require a single bound, and custom signals resolve as booleans with their SQL frozen at creation.
  • `event`{ "type": "event", "kinds": ["split","analyst"], "tickers"?: […] | "universe"?: "slug", "q"?: "…", "event_q"?: "…" } fires when NEW events land. Two composable filters: q is a row-STATE filter on the event's ticker (market_cap > 1e10), event_q an event-CONTENT filter in the /v2/events grammar over (ticker, ts, kind, payload). Delivers events.fired; latency is ingest cadence (analyst within the hour, corporate kinds daily). See Subscribe to events.
  • The per-resource subscribe endpoints (POST /v2/tickers/{T}/subscribe, /v2/signals/{S}/subscribe, /v2/scan/subscribe, /v2/events/subscribe) are permanent shorthand for this endpoint — every door builds the identical webhook object, listed, updated, and deleted at /v2/webhooks.
  • Wire-delivery responses (webhook / Discord / mobile-push) include a test_url. Creating does NOT auto-fire — call it to validate your receiver. In-app subscriptions have nothing to validate and carry no test_url.
  • Prefer the subscribe shorthand when working from a resource: POST /v2/tickers/{T}/subscribe etc. — same object, fewer keystrokes.
Ticker trigger — one symbol, one condition
curl -X POST "https://api.tickerbot.io/v2/webhooks" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger": { "type": "ticker", "ticker": "NVDA", "condition": "rsi_14 > 70 AND relative_volume > 2" },
    "target_url": "https://example.com/hook"
  }'
Response
{
  "as_of": "2026-07-27T15:41:00.000Z",
  "id": "wh_smRsF3-z36o",
  "name": "NVDA: rsi_14 > 70 AND relative_volume > 2",
  "q": "ticker = 'NVDA' AND (rsi_14 > 70 AND relative_volume > 2)",
  "status": "active",
  "subscription_origin": { "type": "ticker", "ref": "NVDA", "condition": "rsi_14 > 70 AND relative_volume > 2" },
  "test_url": "/v2/webhooks/wh_smRsF3-z36o/test"
}
Signal trigger — numeric signal with a bound, scoped to a universe
curl -X POST "https://api.tickerbot.io/v2/webhooks" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger": { "type": "signal", "signal": "rsi_14", "condition": ">70", "universe": "top_100" },
    "channel": "discord",
    "discord_url": "https://discord.com/api/webhooks/123456789012345678/aBcDeF…"
  }'
Response
{
  "as_of": "2026-07-27T15:42:00.000Z",
  "id": "wh_dC9rTq-k77p",
  "name": "rsi_14 >70 · top_100",
  "q": "rsi_14 > 70",
  "universe_id": "top_100",
  "channel": "discord",
  "status": "active",
  "subscription_origin": { "type": "signal", "ref": "rsi_14", "condition": ">70" },
  "test_url": "/v2/webhooks/wh_dC9rTq-k77p/test"
}
Event trigger — Goldman downgrades on large caps
curl -X POST "https://api.tickerbot.io/v2/webhooks" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger": {
      "type": "event",
      "kinds": ["analyst"],
      "q": "market_cap > 1e10",
      "event_q": "payload->>'"'"'firm'"'"' = '"'"'Goldman Sachs'"'"'"
    },
    "target_url": "https://example.com/hook"
  }'
Response
{
  "as_of": "2026-07-27T15:43:00.000Z",
  "id": "wh_e9Kp3q-r52s",
  "name": "events: analyst",
  "q": "market_cap > 1e10",
  "trigger_kind": "event",
  "event_kinds": ["analyst"],
  "status": "active",
  "subscription_origin": { "type": "event", "ref": "analyst", "condition": "market_cap > 1e10" },
  "test_url": "/v2/webhooks/wh_e9Kp3q-r52s/test"
}

Shortcuts

Four convenience endpoints build the identical webhook. Reach for whichever reads best.

Each of these is permanent sugar over POST /v2/webhooks: it parses a simpler body into a trigger and mints the same webhook object, listed and managed at /v2/webhooks like any other.