TradeMCP News Docs

Alerts and webhooks

Rules that POST a signed webhook to your URL the moment a matching article arrives.

Reading rules and deliveries and dry runs need alerts:read; creating, changing and deleting rules and retrying deliveries need alerts:write (which includes alerts:read). Every endpoint only ever sees your own organization's rules. You can also manage rules and inspect deliveries in the dashboard.

Manage rules

MethodPath
GET/v1/alerts/rulesList your rules
POST/v1/alerts/rulesCreate a rule. The response includes its webhook_secret, shown once. 409 when your plan's rule limit is reached
PATCH/v1/alerts/rules/{id}Replace name, criteria, URL or enabled
DELETE/v1/alerts/rules/{id}Delete
POST/v1/alerts/rules/{id}/testDry run against the last 24 hours of articles
GET/v1/alerts/deliveries?status=Delivery log
POST/v1/alerts/deliveries/{id}/retrySend a failed (or delivered) webhook again, resetting its attempts. 10 per minute

Rule fields

Body of POST and PATCH (PATCH replaces the whole rule, so send every field).

FieldTypeRequiredDefaultNotes
namestringyes1 to 200 characters
criteriaobjectyesSee Criteria. Needs at least one condition
webhook_urlURLyesMust resolve to a public address
enabledbooleannotrueDisabled rules never fire

Criteria

All conditions you set must hold (logical AND), and a rule needs at least one of keywords_any, keywords_all, symbols, source_ids, sentiment, min_abs_sentiment or min_impact. Every field is optional.

FieldTypeValuesDefaultMatches when
keywords_anystring[]words, phrases, prefix*[]At least one keyword appears
keywords_allstring[]words, phrases, prefix*[]Every keyword appears
excludestring[]words, phrases, prefix*[]None of them appears. Checked on its own, so it does not count as a condition
symbolsstring[]tickers as listed by /v1/assets, case-insensitive, e.g. BTC, EURUSD, XAUUSD[]The article is tagged with at least one of them
sentimentstringpositive, negative, neutralnoneThe article's sentiment label equals it
min_abs_sentimentnumber0 to 1noneThe sentiment score is at least this strong in either direction
min_impactnumber0 to 1noneThe article's impact score is at least this
source_idsstring[]source ids (the source_id field on articles from news search)[]The article comes from one of these sources
include_duplicatesbooleantrue, falsefalsefalse skips articles flagged as duplicates of an earlier story

Out-of-range numbers or an unknown sentiment value are rejected with 422.

Keyword matching

  • Matching runs over the title and summary, case-insensitively.
  • A plain word matches whole words only: sec matches "SEC sues" but not "sector".
  • A phrase matches as written: "rate cut".
  • A trailing * matches any word starting with the text: hack* matches "hack", "hacked" and "hackers".

Examples

Set KEY to your API key. Each example is one POST /v1/alerts/rules call.

1. Simplest: one keyword

curl -s https://news.trademcp.dev/v1/alerts/rules \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"name": "Anything on ETFs",
       "criteria": {"keywords_any": ["etf"]},
       "webhook_url": "https://example.com/hooks/datap"}'

The response contains the rule id and its webhook_secret. Store the secret now, it is not shown again.

2. One asset, strong moves only

{"name": "BTC negative news",
 "criteria": {"symbols": ["BTC"], "sentiment": "negative", "min_abs_sentiment": 0.5},
 "webhook_url": "https://example.com/hooks/datap"}

3. Keywords with exclusions

{"name": "Exchange security incidents",
 "criteria": {"keywords_any": ["hack*", "exploit*", "breach*", "outage"],
              "keywords_all": ["exchange"],
              "exclude": ["rumour*", "unconfirmed"]},
 "webhook_url": "https://example.com/hooks/datap"}

4. Real-world: FX desk, high-impact central bank news

Alert when a major central bank story touches your pairs, from chosen sources only, with duplicates and rumours filtered out.

{"name": "FX desk: central bank surprises",
 "criteria": {
   "keywords_any": ["rate cut", "rate hike", "rate decision", "intervention", "hawkish", "dovish"],
   "exclude": ["rumour*", "speculat*", "opinion"],
   "symbols": ["EURUSD", "GBPUSD", "USDJPY", "USD"],
   "min_impact": 0.6,
   "min_abs_sentiment": 0.4,
   "source_ids": ["<reuters-source-id>", "<bloomberg-source-id>"],
   "include_duplicates": false},
 "webhook_url": "https://trading.example.com/hooks/datap",
 "enabled": true}

5. Real-world: crypto risk monitor, one rule per concern

Rules are independent, so split concerns into separate rules and route each to its own URL:

{"name": "Regulatory: SEC and ETFs",
 "criteria": {"keywords_any": ["sec", "etf*", "lawsuit", "ban*"], "symbols": ["BTC", "ETH"], "min_impact": 0.4},
 "webhook_url": "https://example.com/hooks/compliance"}
{"name": "Security: hacks and depegs",
 "criteria": {"keywords_any": ["hack*", "exploit*", "depeg*", "drain*"], "sentiment": "negative", "min_impact": 0.3},
 "webhook_url": "https://example.com/hooks/security-pager"}

Try it before going live

Dry-run any rule against the last 24 hours of articles. It returns the match count and up to 20 sample hits, and sends nothing.

curl -s -X POST https://news.trademcp.dev/v1/alerts/rules/$RULE_ID/test -H "X-API-Key: $KEY"
{"matched": 7, "sample": [{"id": "…", "title": "…", "published_at": "…"}]}

Too many matches? Raise min_impact, add exclude words, or narrow symbols. Too few? Switch keywords_all to keywords_any, use prefix*, or lower the thresholds.

Delivery

We POST JSON to your URL:

{"type": "article.matched", "rule_id": "0f3c…", "delivery_id": "…", "attempt": 1,
 "article": {"title": "…", "source_name": "…", "symbols": ["EUR", "EURUSD"], "sentiment_label": "negative",
             "sentiment_score": -0.41, "impact": 0.66, "url": "https://…"}}

with these headers:

  • X-Datap-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
  • X-Datap-Delivery: <delivery id>: use it to ignore retries you have already processed

Return any 2xx within 10 seconds. Failed deliveries are retried after 30 s, 1 min, 2 min, 4 min and so on, capped at 1 hour, for up to 8 attempts. In the delivery log, status is pending (will retry), delivered or failed (gave up). URLs resolving to private, loopback or link-local addresses are refused.

Each delivery in the log has last_status_code and an error_class describing the last failure (your endpoint's response body is never echoed back):

error_classMeaning
http_<code>Your endpoint answered with that non-2xx status, e.g. http_500
timeoutNo response within 10 seconds
connect_errorDNS failure, refused or reset connection
tls_errorTLS handshake or certificate verification failed
unsafe_targetThe URL resolves to a non-public address; fix the rule's URL (such deliveries cannot be retried)

Fixed your endpoint? POST /v1/alerts/deliveries/{id}/retry queues the delivery again immediately (409 if it is already queued or the target is unsafe).

Verify the signature

Check the signature against the raw request body and reject timestamps older than 5 minutes:

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, parts["v1"])

On this page