Skip to content

Examples

Three ways to read the same thing: the key, the organizations, the projects, the stack, and the alerts triggered since yesterday. None of them writes.

The OpenAPI 3.1 specification of the public API can be downloaded here: openapi-v1.json. It always matches the version in service. The organization API key is created in the organization settings, API tab, and sent as Authorization: Bearer twa_…. See Authentication.

curl.sh runs five GET requests. TWA_ORG_ID and TWA_PROJECT_ID are the identifiers returned by the first two calls. TWA_SINCE, when set, is an ISO 8601 instant; otherwise the script uses the last 24 hours.

#!/usr/bin/env bash
# Lecture seule de l'API publique. Aucune écriture.
# Variables : TWA_API_KEY, TWA_ORG_ID, TWA_PROJECT_ID.
# TWA_SINCE (optionnel) : instant ISO 8601, sinon les dernières 24 heures.
set -euo pipefail
BASE="${TWA_BASE_URL:-https://app.techwatchalert.com}"
auth="Authorization: Bearer ${TWA_API_KEY:?}"
if [[ -z "${TWA_SINCE:-}" ]]; then
TWA_SINCE="$(python3 -c 'from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ"))')"
fi
curl -fsS -H "$auth" "$BASE/api/v1/me"
curl -fsS -H "$auth" "$BASE/api/v1/organizations"
curl -fsS -H "$auth" "$BASE/api/v1/organizations/${TWA_ORG_ID:?}/projects"
curl -fsS -H "$auth" "$BASE/api/v1/organizations/${TWA_ORG_ID}/projects/${TWA_PROJECT_ID:?}/stack"
curl -fsS -G -H "$auth" \
--data-urlencode "since=${TWA_SINCE}" \
--data-urlencode "limit=50" \
"$BASE/api/v1/organizations/${TWA_ORG_ID}/projects/${TWA_PROJECT_ID}/alerts"

The server is https://mcp.techwatchalert.com/mcp. The calls in mcp.json are read-only tools: organizations, projects, stack, project alerts, and a search of the CVE database. project accepts the project identifier or its exact name.

{
"server": "https://mcp.techwatchalert.com/mcp",
"appels": [
{
"name": "list_organizations",
"arguments": {}
},
{
"name": "list_projects",
"arguments": {}
},
{
"name": "get_project_stack",
"arguments": {
"project": "production"
}
},
{
"name": "list_project_alerts",
"arguments": {
"project": "production",
"sort": "date",
"limit": 20
}
},
{
"name": "search_cves",
"arguments": {
"query": "openssl",
"severities": ["CRITICAL"],
"limit": 5
}
}
]
}

Every call carries two headers:

  • X-TWA-Timestamp: the send time, as Unix seconds;
  • X-TWA-Signature: sha256= followed by the HMAC-SHA256 of the secret over timestamp + "." + raw body.

The body is the one received, not a reformatted copy. A timestamp five minutes (300 seconds) or more off is rejected. The sender does not remember signatures already delivered: a copy resent inside that window still verifies. The secret is shown once, when the webhook is created, and on each regeneration.

verifier_signature.py recomputes the same value the sender used. See also Webhook signature.

The cve.exploitee event is sent once per CVE and per project. The call is signed with X-TWA-Signature. cvss_score and epss_score are null when the score is unpublished: that is not a zero.

{
"event": "cve.exploitee",
"project_id": "…",
"triggered_at": "2026-10-13T18:00:00+00:00",
"cve_id": "CVE-2026-0001",
"severity": "CRITICAL",
"cvss_score": null,
"epss_score": null,
"has_poc": false,
"is_kev": true,
"rancongiciel": false,
"description": "…",
"matched_type": "product",
"matched_value": "openssl"
}
"""Vérifie X-TWA-Signature sur le corps brut d'un webhook TechWatchAlert.
Même calcul que l'envoi (app.core.outbox.signer_webhook) : HMAC-SHA256 du
secret sur ``X-TWA-Timestamp + "." + corps``, préfixe ``sha256=``. Le corps
est celui reçu, octets pour octets. Un horodatage écarté de 300 secondes ou
plus est refusé. L'envoi ne mémorise pas les signatures déjà parties : une
copie renvoyée dans cette fenêtre se vérifie encore.
Usage : python3 verifier_signature.py SECRET HORODATAGE ENTETE < corps
Code 0 si la signature est bonne, 1 si elle est refusée, 2 si l'usage est faux.
"""
from __future__ import annotations
import hashlib
import hmac
import sys
import time
FENETRE_SECONDES = 300
def signature(secret: str, horodatage: str, corps: bytes) -> str:
mac = hmac.new(secret.encode(), horodatage.encode() + b"." + corps, hashlib.sha256)
return "sha256=" + mac.hexdigest()
def verifier(
secret: str,
horodatage: str,
corps: bytes,
entete: str,
*,
maintenant: float | None = None,
) -> bool:
if not horodatage.isdigit():
return False
instant = time.time() if maintenant is None else maintenant
if abs(instant - int(horodatage)) >= FENETRE_SECONDES:
return False
return hmac.compare_digest(signature(secret, horodatage, corps), entete)
def main() -> int:
if len(sys.argv) != 4:
print(
"usage: verifier_signature.py SECRET HORODATAGE ENTETE < corps",
file=sys.stderr,
)
return 2
secret, horodatage, entete = sys.argv[1:]
corps = sys.stdin.buffer.read()
ok = verifier(secret, horodatage, corps, entete)
print("ok" if ok else "refus")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())

nouvelles-cve-stack.yml is an example to copy into .github/workflows/ in your repository, with nouvelles_cve_stack.py in .github/scripts/ and a TWA_API_KEY secret. It runs every day, or on demand. The repository permission is contents: read. The job only calls the organization, project and alert GETs. It fails when there is at least one alert in the window, so the scheduled run shows it. Nothing is written to the repository, and the API is not called to write.

TWA_SINCE bounds the window. TWA_ORG_ID and TWA_PROJECT_ID narrow the walk; empty, the key sees everything it is allowed to read.

The test suite replays these examples: a renamed route, a removed tool or a signature that no longer matches the sender fails the suite.