Alerts & Intelligence Engine
Define price, technical and volume triggers once; ApexTick evaluates them on every fresh tick and delivers the event to email, signed webhooks, Telegram, Discord, Slack or a REST event log.
You don't need to keep a WebSocket open or poll for prices. Create a trigger with POST /v1/alerts; ApexTick evaluates it in the background on every accepted tick and delivers an event when the condition is met.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /v1/alerts | Create an alert trigger |
GET | /v1/alerts | List active triggers and your quota usage |
DELETE | /v1/alerts/{id} | Deactivate a trigger and free its quota slot |
GET | /v1/alerts/events | Delivery history of fired alerts (90 days) |
| Plan | Active alerts | Channels |
|---|---|---|
| Hobbyist | 3 | email |
| Trader Pro | 50 | email, webhook, telegram, discord, slack, rest |
| Quant & Community | 250 | All 6 channels |
| Enterprise | Unlimited | All 6 channels |
Create an alert
/v1/alertsHobbyist+curl -s -X POST "https://api.apextick.com/v1/alerts" \
-H "apikey: $APEXTICK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"symbol": "AAPL",
"trigger_type": "technical",
"metric": "rsi14",
"condition": "lte",
"threshold": 30,
"channel": "telegram",
"target": "@my_trading_channel",
"cooldown_minutes": 60
}'
Request fields
| Field | Type | Used by | Description |
|---|---|---|---|
symbol | string | All | Instrument in standard notation, e.g. AAPL, BTC-USD. |
trigger_type | string | All | price, technical, volume, news or calendar. |
metric | string | price, technical, volume | price for price triggers, rvol20 for volume triggers, or a technical metric. |
condition | string | price, technical, volume | gt, lt, gte, lte, crosses_above or crosses_below. |
threshold | number | price, technical, volume | Fixed reference value. Must be positive for price and volume triggers. |
compare_metric | string | price, technical | Compare against another indicator instead of a fixed threshold. Use one or the other. |
sentiment | string | news | bullish, bearish or neutral. |
category | string | news | earnings, macro, mergers, regulatory or technical_breakout. |
event | string | calendar | earnings, dividend or split. |
notice_hours_before | integer | calendar | Notice period, 1–168 hours. Default 24. |
channel | string | All | email, webhook, telegram, discord, slack or rest. |
target | string | All except rest | Where to deliver; see channels. |
cooldown_minutes | integer | All | Minimum time between deliveries, 1–10080. Default 60. |
Response
201 Created returns the stored trigger. For the webhook channel the response includes a signing_secret. It is returned only once, so store it now; you need it to verify deliveries.
{
"id": "6f1c2a9e-3b4d-4c8a-9e21-7d5b0c4e8f13",
"symbol": "NVDA",
"trigger_type": "volume",
"metric": "rvol20",
"condition": "crosses_above",
"threshold": 2,
"channel": "webhook",
"target": "https://hooks.example.com/apextick",
"signing_secret": "whsec_3f9a…",
"cooldown_minutes": 60,
"active": true,
"created_at": "2026-10-16T13:05:12Z"
}
If you exceed your plan's alert quota or use a channel your plan doesn't include, the API returns 403 with the reason, for example {"error": "Active alert limit reached for current plan", "limit": 3}.
Trigger types
Price
Fires on the last traded price. Use metric: "price" with a positive threshold, or compare the price with a moving average using compare_metric:
{ "symbol": "TSLA", "trigger_type": "price", "metric": "price", "condition": "crosses_below", "threshold": 240, "channel": "email", "target": "[email protected]" }
{ "symbol": "SPY", "trigger_type": "price", "metric": "price", "condition": "crosses_above", "compare_metric": "sma200", "channel": "slack", "target": "https://hooks.slack.com/services/T000/B000/XXXX" }
Technical
Fires on a technical indicator. Available metrics:
| Metric | Description |
|---|---|
sma20, sma50, sma200 | Simple moving averages over 20, 50 and 200 sessions |
rsi14 | 14-period Relative Strength Index (0–100) |
macd14 | 14-day MACD value |
macd, macd_signal, macd_hist | MACD 12/26/9 line, signal line and histogram on daily closes |
atr14 | 14-period Average True Range |
stoch_k14 | 14-period stochastic %K |
volatility30 | 30-day historical volatility |
Classic setups:
{ "symbol": "AAPL", "trigger_type": "technical", "metric": "rsi14", "condition": "crosses_below", "threshold": 30, "channel": "discord", "target": "https://discord.com/api/webhooks/123/abc" }
Golden Cross, the 50-day average crossing above the 200-day:
{ "symbol": "MSFT", "trigger_type": "technical", "metric": "sma50", "condition": "crosses_above", "compare_metric": "sma200", "channel": "webhook", "target": "https://hooks.example.com/apextick" }
Bullish MACD signal-line cross:
{ "symbol": "AAPL", "trigger_type": "technical", "metric": "macd", "condition": "crosses_above", "compare_metric": "macd_signal", "channel": "telegram", "target": "@my_trading_channel" }
Volume
Fires on relative volume. rvol20 is today's session volume divided by the average daily volume of the last 20 completed sessions, so 2 means 200% of normal volume. Use crosses_above to catch the onset of a surge:
{ "symbol": "NVDA", "trigger_type": "volume", "metric": "rvol20", "condition": "crosses_above", "threshold": 2, "channel": "webhook", "target": "https://hooks.example.com/apextick" }
compare_metric is not supported for volume triggers.
News and calendar
News and calendar triggers are validated, stored and count towards your alert quota today, but they don't fire yet. They will activate automatically once the news and corporate-calendar feeds are live, with no change to your triggers.
News triggers filter headlines by sentiment and category:
{ "symbol": "TSLA", "trigger_type": "news", "sentiment": "bearish", "category": "regulatory", "channel": "webhook", "target": "https://hooks.example.com/news" }
Calendar triggers notify you ahead of an earnings, dividend or split date:
{ "symbol": "NVDA", "trigger_type": "calendar", "event": "earnings", "notice_hours_before": 24, "channel": "discord", "target": "https://discord.com/api/webhooks/123/abc" }
Conditions and cooldowns
| Condition | Kind | Fires |
|---|---|---|
gt, lt, gte, lte | Level | While the condition is true, at most once per cooldown_minutes. |
crosses_above, crosses_below | Crossing | Once, on the tick where the value crosses the reference. It fires again only after crossing back and crossing again. |
The reference is either a fixed threshold or the live value of compare_metric. A trigger must use exactly one of them, and compare_metric must differ from metric.
Only fresh ticks are evaluated: stale or out-of-order data never fires an alert, and evaluation doesn't depend on you having a stream open.
Delivery channels
| Channel | target | Delivered as |
|---|---|---|
email | One plain email address, e.g. [email protected] | |
webhook | A public https URL | Signed JSON POST of the event |
telegram | @channel_name or a numeric chat id | Telegram message |
discord | A Discord webhook URL (https://discord.com/api/webhooks/…) | Discord message |
slack | A Slack incoming webhook URL (https://hooks.slack.com/services/…) | Slack message |
rest | None (omit target) | Recorded only; read it from GET /v1/alerts/events |
Webhook-style targets must be public https URLs. Private and internal addresses are rejected, and redirects are not followed. Failed deliveries caused by network errors, 408, 429 or 5xx responses are retried with exponential backoff.
The message text sent to email, Telegram, Discord and Slack looks like this:
ApexTick alert: NVDA rvol20 is 2.34 (crossed above 2) as of 2026-10-16T14:30:05Z
Webhook payload
Webhook deliveries POST the alert event as JSON:
{
"alert_id": "6f1c2a9e-3b4d-4c8a-9e21-7d5b0c4e8f13",
"symbol": "AAPL",
"trigger_type": "technical",
"metric": "macd",
"condition": "crosses_above",
"threshold": -2.37,
"compare_metric": "macd_signal",
"observed_value": -2.13,
"tick_timestamp": "2026-10-16T14:30:05Z",
"fired_at": "2026-10-16T14:30:06Z",
"message": "ApexTick alert: AAPL macd is -2.13 (crossed above macd_signal -2.37) as of 2026-10-16T14:30:05Z"
}
threshold carries the reference value at evaluation time: the fixed threshold, or the value of compare_metric. Each request also carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-ApexTick-Alert-Id | The alert's id |
X-ApexTick-Signature | t=<unix seconds>,v1=<hex signature> |
Respond with any 2xx status quickly and do slow work asynchronously.
Verifying webhook signatures
The v1 value is the lowercase hex HMAC-SHA256 of "<t>.<raw body>", keyed with your alert's signing_secret. The timestamp t is part of the signed string, so there is no separate timestamp header. To verify a delivery:
- Parse
tandv1from theX-ApexTick-Signatureheader. - Reject the request if
tis more than five minutes from your clock, to block replays. - Compute the HMAC over the raw request body bytes, before any JSON parsing.
- Compare signatures in constant time.
import hashlib
import hmac
import time
def verify_apextick(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
ts, sig = parts.get("t"), parts.get("v1")
if not ts or not sig or not ts.isdigit():
return False
if abs(time.time() - int(ts)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
# Flask example
# @app.post("/apextick")
# def apextick_webhook():
# if not verify_apextick(SECRET, request.headers.get("X-ApexTick-Signature", ""), request.get_data()):
# abort(401)
# event = request.get_json()
# return "", 204
Digest webhooks are signed the same way; see Briefings & Digests.
List alerts
/v1/alertsHobbyist+Returns your active triggers and quota usage. limit is 0 on plans with unlimited alerts. Signing secrets are never listed.
{
"data": [
{ "id": "6f1c2a9e-…", "symbol": "NVDA", "trigger_type": "volume", "metric": "rvol20", "condition": "crosses_above", "threshold": 2, "channel": "webhook", "target": "https://hooks.example.com/apextick", "cooldown_minutes": 60, "active": true, "created_at": "2026-10-16T13:05:12Z", "last_fired_at": "2026-10-16T14:30:06Z" }
],
"quota": { "used": 1, "limit": 50 }
}
Delete an alert
/v1/alerts/{id}Hobbyist+Deactivates the trigger and frees its quota slot. Returns 204 No Content, or 404 if the alert doesn't exist.
curl -s -X DELETE "https://api.apextick.com/v1/alerts/6f1c2a9e-3b4d-4c8a-9e21-7d5b0c4e8f13" \
-H "apikey: $APEXTICK_API_KEY"
Alert events
/v1/alerts/eventsHobbyist+Lists fired alerts, newest first, with their delivery status: delivered, failed, or recorded for the rest channel. Alerts on the rest channel are delivered only here, which makes this endpoint a simple polling inbox. History is kept for 90 days.
| Query parameter | Description |
|---|---|
alert_id | Only events of this alert. |
limit | Page size, default 50, maximum 200. |
before | Cursor: pass the previous page's next_cursor to get older events. |
{
"data": [
{
"id": 48213,
"alert_id": "6f1c2a9e-3b4d-4c8a-9e21-7d5b0c4e8f13",
"symbol": "NVDA",
"trigger_type": "volume",
"channel": "webhook",
"status": "delivered",
"duration_ms": 142,
"fired_at": "2026-10-16T14:30:06Z",
"event": { "alert_id": "6f1c2a9e-…", "symbol": "NVDA", "metric": "rvol20", "observed_value": 2.34, "message": "ApexTick alert: NVDA rvol20 is 2.34 (crossed above 2) as of 2026-10-16T14:30:05Z" }
}
],
"next_cursor": "48212"
}
Failed deliveries include an error field describing what went wrong.