Webhooks

Receive push notifications when your watchlist changes

Instead of polling the API for changes, register a webhook URL and TickerDB will POST updates to you after each daily and weekly pipeline run. Subscribe to the events you care about and get structured, field-level diffs delivered automatically.

Tier Access
Starter no webhooks. Plus 1 webhook URL. Pro 3 webhook URLs. Business includes 3 webhook URLs per seat.

Event Types

EventDescriptionDefault
watchlist.changesStructured field-level diffs for tickers on your watchlist. Only fires when at least one field has changed.Enabled on creation
data.readySimple notification that fresh data has been computed and is available via the API.Opt-in

Webhook deliveries do not consume your API request quota.

GET — List webhooks

GET https://api.tickerdb.com/v1/webhooks

Returns all registered webhooks for your account. The secret field is never included in GET responses.

Response Fields

FieldTypeDescription
webhooksarrayArray of webhook objects
webhooks[].idstringWebhook ID
webhooks[].urlstringDelivery URL
webhooks[].eventsobjectSubscribed event configuration
webhooks[].activebooleanWhether the webhook is active
webhooks[].created_atstringISO 8601 creation timestamp
webhooks[].updated_atstringISO 8601 last update timestamp
webhook_countintegerNumber of registered webhooks
webhook_limitintegerMax webhooks for your tier
GET /v1/webhooks
{ "webhooks": [ { "id": "d3f1a2b4-...", "url": "https://example.com/webhook", "events": { "watchlist.changes": true, "data.ready": true }, "active": true, "created_at": "2026-03-27T12:00:00.000Z", "updated_at": "2026-03-27T12:00:00.000Z" } ], "webhook_count": 1, "webhook_limit": 1 }

POST — Register a webhook

POST https://api.tickerdb.com/v1/webhooks

Register a new webhook URL. The secret is returned only on creation — save it immediately. Use it to verify webhook signatures.

Request Body

FieldTypeRequiredDescription
urlstringYesHTTPS URL to receive webhook payloads
eventsobjectNoEvent subscriptions. Defaults to {"watchlist.changes": true}
Example Request
{ "url": "https://example.com/webhook", "events": { "watchlist.changes": true, "data.ready": true } }
POST /v1/webhooks
{ "id": "d3f1a2b4-...", "url": "https://example.com/webhook", "secret": "a1b2c3d4e5f6...", "events": { "watchlist.changes": true, "data.ready": true }, "active": true, "created_at": "2026-03-27T12:00:00.000Z" }

PUT — Update a webhook

PUT https://api.tickerdb.com/v1/webhooks

Update the URL, event subscriptions, or active status of an existing webhook.

Request Body

FieldTypeRequiredDescription
idstringYesWebhook ID to update
urlstringNoNew HTTPS URL
eventsobjectNoUpdated event subscriptions
activebooleanNoEnable or disable the webhook
Example Request
{ "id": "d3f1a2b4-...", "events": { "watchlist.changes": true }, "active": false }
PUT /v1/webhooks
{ "updated": true, "id": "d3f1a2b4-..." }

DELETE — Remove a webhook

DELETE https://api.tickerdb.com/v1/webhooks

Request Body

FieldTypeRequiredDescription
idstringYesWebhook ID to delete
Example Request
{ "id": "d3f1a2b4-..." }
DELETE /v1/webhooks
{ "deleted": "d3f1a2b4-...", "webhook_count": 0 }

Verifying Webhook Signatures

Every webhook delivery includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the request body, signed with your webhook's secret. Always verify this signature before processing the payload.

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 hex digest of the raw request body
X-Webhook-EventEvent type: watchlist.changes or data.ready
Content-Typeapplication/json
User-AgentTickerDB-Webhook/2.0

Verification — Python

The secret returned from POST /v1/webhooks is a 64-character hex string. Decode it to raw bytes before using it as the HMAC key.

Python
import hashlib import hmac def verify_webhook(payload: bytes, secret: str, signature: str) -> bool: """Verify an X-Webhook-Signature header. payload — raw request body bytes (before any JSON parsing) secret — hex secret from POST /v1/webhooks (64 hex chars) signature — value of the X-Webhook-Signature header """ key = bytes.fromhex(secret) expected = hmac.new(key, payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) # Flask example from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = "your_secret_here" @app.route("/webhook", methods=["POST"]) def handle(): payload = request.get_data() sig = request.headers.get("X-Webhook-Signature", "") if not verify_webhook(payload, WEBHOOK_SECRET, sig): abort(403) event = request.json # process event["event"], event["data"] ... return "", 200

Verification — Node.js

Node.js
const crypto = require('crypto'); function verifyWebhook(payload, secret, signature) { // payload — raw Buffer or string of the request body // secret — hex secret from POST /v1/webhooks (64 hex chars) // signature — value of X-Webhook-Signature header const key = Buffer.from(secret, 'hex'); const expected = crypto.createHmac('sha256', key) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(signature.padEnd(expected.length, '0'), 'hex'), ); } // Express example app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-webhook-signature'] ?? ''; if (!verifyWebhook(req.body, process.env.WEBHOOK_SECRET, sig)) { return res.sendStatus(403); } const event = JSON.parse(req.body); // process event.event, event.data ... res.sendStatus(200); });

Webhook Payloads

watchlist.changes

Delivered after each pipeline run when at least one field has changed on a watchlist ticker. Contains structured diffs showing exactly which fields changed and their previous/current values.

This payload uses the same changes object as GET /v1/watchlist/changes. Technical fields stay top-level (rsi_zone, macd_state, momentum_direction, divergence_detected, trend_direction, volume_ratio_band, accumulation_state, squeeze_active, extreme_condition, breakout_type). Pro-only stock fundamentals keep a dotted fundamentals.* prefix (fundamentals.valuation_zone, fundamentals.analyst_consensus, fundamentals.earnings_proximity, and related fields). Plus webhooks omit all fundamental diffs. Entitlement is checked again at delivery time, so queued payloads also honor downgrades before delivery.

Webhook deliveries are already scoped to a single run, so unlike the pull endpoint they do not include a ticker_context block.

When a band field changes, the change object includes stability context on Plus Pro tiers: stability, periods_in_current_state, flips_recent, and flips_lookback. These describe the new band value's stability at the time of the change. Not included on Starter tier.

watchlist.changes payload
{ "event": "watchlist.changes", "timestamp": "2026-03-27T21:30:00Z", "data": { "timeframe": "daily", "run_date": "2026-03-27", "changes": { "AAPL": [ { "field": "rsi_zone", "from": "neutral", "to": "oversold", "stability": "fresh", "periods_in_current_state": 1, "flips_recent": 3, "flips_lookback": "30d" }, { "field": "divergence_detected", "from": false, "to": true } ], "TSLA": [ { "field": "macd_state", "from": "contracting_negative", "to": "expanding_positive", "stability": "fresh", "periods_in_current_state": 1, "flips_recent": 2, "flips_lookback": "30d" }, { "field": "accumulation_state", "from": "neutral", "to": "accumulation" }, { "field": "fundamentals.analyst_consensus", "from": "hold", "to": "buy" } ] }, "tickers_checked": 15, "tickers_changed": 2 } }

data.ready

Simple notification that fresh data has been computed. Useful for triggering downstream fetches without polling.

data.ready payload
{ "event": "data.ready", "timestamp": "2026-03-27T21:30:00Z", "data": { "timeframe": "daily", "run_date": "2026-03-27", "tickers_computed": 9847 } }

Delivery History

Every webhook delivery is logged in the database. You can view delivery status, HTTP response codes, and errors from the dashboard or the API.

Dashboard

Open Dashboard → Webhooks, click history next to any webhook row, and the last 20 deliveries expand inline. Status chips show sent (green), failed (red), pending / delivering / retrying (yellow — queued, in flight, or waiting for an automatic retry after a failed attempt), and no changes / skipped (grey — nothing to deliver: no diff for your watchlist, or the watchlist was empty).

GET — List delivery history

GET https://api.tickerdb.com/v1/webhooks/deliveries

Returns delivery records for your account, sorted by run_date descending.

ParameterTypeDescription
webhook_idstringFilter to a single webhook
limitintegerMax records to return. Default 50, max 200.
FieldTypeDescription
deliveries[].idstringDelivery record ID
deliveries[].webhook_idstringWebhook ID
deliveries[].event_typestringwatchlist.changes or data.ready
deliveries[].timeframestringdaily or weekly
deliveries[].run_datestringPipeline run date (YYYY-MM-DD)
deliveries[].statusstringpending, delivering, retrying, sent, endpoint_error (your endpoint never returned a usable response), failed (delivery could not be attempted on our side), no_changes, or skipped
deliveries[].attempt_countintegerNumber of delivery attempts made
deliveries[].http_statusinteger | nullLast HTTP response code from your endpoint
deliveries[].errorstring | nullLast error summary (no secrets)
deliveries[].started_atstringISO 8601 timestamp of first attempt
deliveries[].completed_atstring | nullISO 8601 timestamp of final attempt
GET /v1/webhooks/deliveries
{ "deliveries": [ { "id": "e1f2a3b4-...", "webhook_id": "d3f1a2b4-...", "event_type": "watchlist.changes", "timeframe": "daily", "run_date": "2026-07-02", "status": "sent", "attempt_count": 1, "http_status": 200, "error": null, "started_at": "2026-07-02T21:35:02Z", "completed_at": "2026-07-02T21:35:02Z" } ], "count": 1, "limit": 50 }

Retry & Deactivation

Webhook delivery is handled by a Cloudflare Queue consumer. Failed deliveries are marked retrying and retried automatically with exponential backoff — up to 4 attempts total. After all retries are exhausted the delivery is marked endpoint_error (your endpoint never returned a usable response). The separate failed status is reserved for the rare case where the delivery could not be attempted on our side.

BehaviourDetail
Retry count4 attempts total (initial + 3 retries)
BackoffManaged by Cloudflare Queue (exponential, ~30 s base)
Timeout10 seconds per attempt
Auto-deactivationAfter 7 consecutive undelivered runs (endpoint_error or failed, with no successful delivery in between) the webhook is automatically paused. Re-enable it from the dashboard or via PUT /v1/webhooks with {"active": true}.
Integrity gateWhen INTEGRITY_POLICY=block_publish the pipeline skips webhook delivery entirely for that run. No delivery rows are created and no retries are attempted.
No-changes skipFor watchlist.changes, if no fields changed for your watchlist tickers, no delivery is attempted for that run. The record appears as no_changes in delivery history; an empty watchlist appears as skipped.