REST API · v1
REST over HTTPS, JSON in and out, predictable URLs, cursor-based pagination, conventional HTTP status codes. Authentication is bearer-token with optional request signing for high-trust integrations.
Quickstart
Authenticate, then list high-severity events. Pick your language — every example uses the same token and workspace headers.
SHELLcurl $ curl https://api.threatdefendr.com/v1/events?severity=HIGH \ -H "Authorization: Bearer $TD_TOKEN" \ -H "X-TD-Workspace: ws_acmeProd"
PYTHONlist_events.py from threatdefendr import Client td = Client(token=os.environ["TD_TOKEN"], workspace="ws_acmeProd") for ev in td.events.list(severity="HIGH"): print(ev.id, ev.source)
TYPESCRIPTlist-events.ts import { ThreatDefendr } from "@threatdefendr/sdk"; const td = new ThreatDefendr({ token: process.env.TD_TOKEN, workspace: "ws_acmeProd" }); for await (const ev of td.events.list({ severity: "HIGH" })) { console.log(ev.id, ev.source); }
GOlist_events.go c := td.NewClient(td.Config{Token: os.Getenv("TD_TOKEN"), Workspace: "ws_acmeProd"}) it := c.Events.List(td.EventQuery{Severity: "HIGH"}) for it.Next() { ev := it.Event() fmt.Println(ev.ID, ev.Source) }
Authentication
All requests must include an Authorization: Bearer <token> header. Tokens are scoped — never use a workspace-admin token from a service. Issue scoped tokens at /settings/tokens or via the SDKs.
HTTPcurl $ curl https://api.threatdefendr.com/v1/events \ -H "Authorization: Bearer $TD_TOKEN" \ -H "X-TD-Workspace: ws_acmeProd"
Request signing (optional, recommended)
For ingestion endpoints and webhooks, requests can be additionally signed with Ed25519. The signature covers the timestamp, method, path, and body. Replays older than 5 minutes are rejected.
HTTPsigned POST /v1/events:ingest HTTP/1.1 Host: api.threatdefendr.com Authorization: Bearer $TD_TOKEN X-TD-Signature: ed25519=k7Yt…q9 X-TD-Timestamp: 2026-06-20T14:21:08Z Content-Type: application/json { "events": [ … ] }
Events
Read raw, enriched events as they flow through the fabric. Cursor-paginated; supports server-sent events for tailing.
Sample response · GET /v1/events
JSON200 OK { "data": [ { "id": "ev_8XbJq2nP4", "ts": "2026-06-20T14:21:08.214Z", "severity": "HIGH", "source": "endpoint.process", "actor": { "id": "u_8294", "name": "svc-deploy" }, "target": { "id": "h_47193", "host": "db-prod-02" }, "enrichments": { "asset.tier": "crown-jewel", "intel.adversary": "IRONVEIL" } } ], "next_cursor": "cur_2vK4nT0pQ" }
Query parameters
| Parameter | Type | Description |
|---|---|---|
| severity | stringoptional | Filter by LOW · MED · HIGH · CRITICAL. |
| source | stringoptional | Dotted source selector, e.g. endpoint.process or identity.signin. |
| actor_id | stringoptional | Restrict to a single actor entity. |
| since · until | RFC 3339optional | Time bounds. Defaults to the last 24 hours. |
| cursor | stringoptional | Opaque cursor from a previous next_cursor. |
| limit | integeroptional | Page size, 1–1000. Default 100. |
▸ Event object
| id | string | Stable event identifier (ev_…). |
| ts | RFC 3339 | Event time, millisecond precision, UTC. |
| severity | enum | LOW · MED · HIGH · CRITICAL. |
| source | string | Originating stream, dotted. |
| actor · target | object | Resolved entity refs (id, plus name / host). |
| enrichments | object | Fabric-added context — asset tier, intel tags, geo. |
Pagination & filtering
List endpoints are cursor-paginated. A response includes next_cursor when more records exist; pass it back as the cursor parameter for the next page. A null cursor means you've reached the end.
SHELLpaginate $ curl "https://api.threatdefendr.com/v1/events?limit=100&cursor=cur_2vK4nT0pQ" \ -H "Authorization: Bearer $TD_TOKEN"
Filter with the query parameters above; combine freely. Results return newest-first — add sort=asc to reverse. Cursors encode the active filter set, so don't change filters mid-pagination.
Detections
Define, deploy, version, and roll back behavioral detections. Detection-as-code workflows use the YAML format; the API accepts both YAML and compiled JSON plans.
Sample request · POST /v1/detections
JSONrequest { "id": "svc-account-from-corp-ip", "title": "Service account authenticated from corp IP", "severity": "HIGH", "plan": { "stream": "identity.signin", "match": { "actor.type": "service_account", "net.src_geo.cidr_label": "corp-egress" }, "window": "5m" }, "on_match": { "create_case": true, "contain": { "action": "disable-actor", "requires_approval": true } } }
Request body
| Field | Type | Description |
|---|---|---|
| id | stringrequired | Stable slug, unique per workspace. Used for updates and rollbacks. |
| title | stringrequired | Human-readable name shown on cases and alerts. |
| severity | enumrequired | LOW · MED · HIGH · CRITICAL. |
| plan | objectrequired | The match logic: stream, match predicate, and window. |
| on_match | objectoptional | Response wiring: create_case, contain, notify. |
Cases
Investigation timelines, including roll-ups across detections, response actions, and analyst notes. Cases bind to entities, not to single events.
Sample response · GET /v1/cases/:id
JSON200 OK { "id": "case_7Qd2Rk9", "status": "open", "severity": "HIGH", "title": "Service account from corp egress", "owner": "alex@acme.io", "entities": [ "u_8294", "h_47193" ], "timeline": [ { "ts": "2026-06-20T14:21:08Z", "kind": "detection", "ref": "det_2vK4nT" }, { "ts": "2026-06-20T14:21:41Z", "kind": "action", "ref": "act_9bc01" } ], "created_at": "2026-06-20T14:21:08Z" }
Query parameters
| Parameter | Type | Description |
|---|---|---|
| status | enumoptional | open · investigating · contained · closed. |
| owner | stringoptional | Filter by assigned analyst. |
| severity | enumoptional | LOW · MED · HIGH · CRITICAL. |
| since · until | RFC 3339optional | Filter by case creation time. |
| cursor · limit | string · intoptional | Standard cursor pagination. |
▸ Case object
| id | string | Stable case identifier (case_…). |
| status | enum | Lifecycle state — open through closed. |
| entities | array | Entity ids the case binds to — actors and targets. |
| timeline | array | Ordered events, detections, and actions with refs. |
| owner | string | Assigned analyst; null when unassigned. |
Containment
Direct, idempotent response actions. Every call records its inverse in the case timeline so undo is single-call.
Sample request · POST /v1/contain:isolate-host
JSONrequest { "host_id": "h_47193", "reason": "IRONVEIL signed-driver match", "case_id": "case_7Qd2Rk9", "requires_approval": false }
Sample response · 200 OK
JSONaction recorded { "action_id": "act_9bc01", "type": "isolate-host", "state": "applied", "inverse": "release-host", "case_id": "case_7Qd2Rk9", "applied_at": "2026-06-20T14:21:41Z" }
Request body
| Field | Type | Description |
|---|---|---|
| host_id · actor_id | stringrequired | Target of the action; which field depends on the endpoint. |
| reason | stringrequired | Free-text justification, written to the case timeline. |
| case_id | stringoptional | Attach the action to an existing case. |
| requires_approval | booleanoptional | Queue for analyst sign-off instead of acting immediately. |
Every action returns an inverse — the single endpoint that undoes it. Replay that endpoint, or call /v1/contain:rollback with the action_id, to reverse cleanly.
Idempotency
Every state-changing POST accepts an Idempotency-Key header. Reusing a key within 24 hours returns the original response instead of acting again — safe to retry on network failures without double-containing a host.
HTTPidempotent $ curl "https://api.threatdefendr.com/v1/contain:isolate-host" \ -H "Authorization: Bearer $TD_TOKEN" \ -H "Idempotency-Key: 5f3c…b1" \ -d '{"host_id":"h_47193"}'
Keys are scoped per endpoint and token. A retried key with a different body returns 409 Conflict.
Adversaries
Read-only access to the tracked-adversary catalog. Updated continuously by the Intelligence Desk.
Sample response · GET /v1/adversaries/:code
JSON200 OK { "code": "IRONVEIL", "aliases": [ "APT-4471", "Silent Forge" ], "motivation": "espionage", "first_seen": "2023-11-02", "last_seen": "2026-06-18", "ttps": [ "T1059.001", "T1078", "T1021.006" ], "ioc_count": 1284, "confidence": "high" }
▸ Adversary object
| code | string | Stable tracking codename. |
| aliases | array | Names used by other vendors and reports. |
| ttps | array | MITRE ATT&CK technique ids. |
| ioc_count | integer | Live indicator count; pull the feed at /iocs. |
| confidence | enum | Attribution confidence — low · medium · high. |
Webhooks
Receive case-state and detection-match events at your HTTPS endpoint. Webhooks are Ed25519-signed; verify with the SDK or the snippet below.
Verifying a webhook signature
PYTHONverify.py from threatdefendr import verify_webhook @app.post("/td/webhook") def incoming(req): body = req.get_data() sig = req.headers["X-TD-Signature"] ts = req.headers["X-TD-Timestamp"] verify_webhook(secret=WH_SECRET, body=body, signature=sig, timestamp=ts) handle(json.loads(body))
Event types
| Event | Fires when |
|---|---|
| detection.matched | A detection fires against the live stream. |
| case.opened | A new case is created. |
| case.updated | Status, owner, or severity changes. |
| action.applied · action.reversed | A containment action is taken or undone. |
Sample payload · case.opened
JSONdelivered to your endpoint { "event": "case.opened", "delivery_id": "dlv_8Xa2Qr", "ts": "2026-06-20T14:21:08Z", "data": { "case_id": "case_7Qd2Rk9", "severity": "HIGH", "title": "Service account from corp egress" } }
Deduplicate on delivery_id and verify the X-TD-Signature header before processing — see the webhook security guide.
Errors
The API uses conventional HTTP status codes. All 4xx and 5xx responses share a single envelope:
JSONerror envelope { "error": { "code": "invalid_detection_plan", "message": "`window` must be a duration ≤ 24h", "field": "plan.window", "request_id": "req_2vK4nT0pQ" } }
| Status | Meaning | Typical fix |
|---|---|---|
400 | Validation error | Read error.field and resubmit. |
401 | Missing or invalid token | Refresh credentials. |
403 | Token lacks required scope | Re-issue with broader scope. |
404 | Resource doesn't exist or not visible | Check workspace scope. |
409 | Conflict (idempotency / state) | Retry with new Idempotency-Key. |
429 | Rate-limited | Honor Retry-After. |
5xx | Server-side | Retry with exponential back-off. |
Rate limits
Per-token bucket; refilled continuously. Limits are returned on every response:
HTTPheaders X-RateLimit-Limit: 300 X-RateLimit-Remaining: 287 X-RateLimit-Reset: 1719256968 X-RateLimit-Bucket: events.read
| Bucket | Default | Burst |
|---|---|---|
events.read | 300 / min | 600 |
events.ingest | 20,000 / s | 40,000 |
detections.write | 60 / min | 120 |
cases.write | 120 / min | 240 |
contain.write | 30 / min | 60 |
Enterprise customers get custom limits — talk to your account team or open a ticket with the workload profile.
Versioning & stability
The API is versioned in the URL path (/v1). Backward-compatible changes — new fields, endpoints, and enum values — ship without a version bump, so write tolerant parsers. Breaking changes ship under a new major version.
| Tier | Meaning | Change policy |
|---|---|---|
● stable | Production-ready, SLA-backed | No breaking changes within a major version. |
◐ beta | Usable; shape may still shift | Two weeks' notice before breaking changes. |
○ experimental | Preview, opt-in via header | May change or be withdrawn without notice. |
Deprecations are announced at least six months ahead. Sunset endpoints return a Sunset header with the removal date and a link to the migration guide.