← tickerbot.io
View as markdown

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 every non-boolean signal — a single bound typed like the signal: >70 (numeric), <YYYY-MM-DDTHH:MM:SSZ (timestamp), >=YYYY-MM-DD (date), =ETF (string); 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

Signal 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.

as_ofstring

Server time this response was assembled (ISO 8601).

idstring

The webhook id — `wh_…`, the handle for every other call on this record.

namestring

Your label for the subscription.

qstring

The stored predicate. Custom signals appear expanded: the SQL is frozen at creation.

rule_idstring

Legacy link to a v1 alert rule; `null` on everything created through v2.

fieldsstring

Extra signals carried on each fired match row; `null` means the standard set.

orderstring

Sort signal for the payload row list; `null` means the evaluator default (`market_cap`).

dirstring

Sort direction for that list; `null` means the default (`desc`).

universe_idstring

Universe the trigger is scoped to, or `null` for the whole market.

cadencestring

How often the trigger is evaluated — `realtime`, `hourly`, or `nyse_open`.

channelstring

Where deliveries go: `webhook`, `discord`, `in_app`, or `mobile_push`.

target_urlstring

Your HTTPS endpoint; `null` on every channel except `webhook`.

deliverystring

Legacy alias of `channel`, kept aligned for older readers.

statusstring

`active` or `disabled`. Auto-disable follows repeated delivery failure.

sourcestring

Which API version created the record; `v2` for anything you create today.

subscription_originobject

Which door created it — `type` (`ticker`/`signal`/`scan`/`event`), its `ref`, and the `condition` in display form.

last_predicate_valuestring

The trigger's value at the last evaluation; `null` until it has run.

created_atnumber

Creation timestamp.

updated_atnumber

Last modification timestamp.

last_firednumber

When a delivery last went out; `null` if it never has.

last_match_setarray

Tickers matching at the last evaluation — the set the next run is diffed against, which is what makes firing edge-triggered.

last_eval_error_atnumber

When the last evaluation error happened; `null` on a healthy hook.

last_errorstring

The last evaluation error; `null` on a healthy hook. The answer to "why is my webhook not firing?".

next_eval_atnumber

When the evaluator will next consider this subscription.

last_evaluated_atnumber

When it was last evaluated; `null` until the first run.

event_kindsarray

Event-trigger webhooks only: the kinds subscribed (`split`, `dividend`, `insider`, `analyst`, `earnings`).

event_qstring

Event-trigger webhooks only: the payload filter, or `null`.

event_tickersarray

Event-trigger webhooks only: the symbols the trigger is scoped to, or `null` for the universe / whole market.

trigger_kindstring

Event-trigger webhooks only: `event`.

channel_configobject

Returned on create only: the channel-specific delivery settings as stored (e.g. the Discord URL, the device id).

signing_secretstring

Returned on create only — shown once, never again. HMAC key for verifying the `X-Tickerbot-Signature` header on deliveries.

test_urlstring

Returned on create only: the `POST /v2/webhooks/{id}/test` URL for this record.

_metaobject

Returned on create only, and only when the rule or `columns` named a column under its pre-2026-09-07 spelling: `deprecated_columns` lists each one (`requested`, `use`, `note`). The stored rule carries the current name.

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_implemented — channel: "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, every other type requires a single bound typed like the signal (>70, <YYYY-MM-DDTHH:MM:SSZ, >=YYYY-MM-DD, =ETF), 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.