Forward EDR / SIEM telemetry into Threxen — in one POST.
Threxenaccepts telemetry from any forwarder that can speak HTTP. Mint a per-tenant API key, send it as a Bearer token, and one event lands as one sealed audit record on your tenant's watchdog pane. Autonomous cyber defense agents that hunt, contain, and deceive — built for regulated enterprises operating under HIPAA, PCI, FedRAMP, DORA, and NIS2.
Authentication
A single Bearer header. Nothing else.
The ingest endpoint is public on the wire, gated only by a per-tenant API key. Mint a key at /app/settings/api and pass it as Authorization: Bearer <token>. The forwarder can ship from a CI runner, a SIEM bridge, or any host able to do TLS.
Tenant-scoped
Each key carries the tenant it was issued for. Per-tenant isolation is enforced at the data plane — a key from tenant A cannot bend ingestion toward tenant B.
Revocable
Revoke at /app/settings/api — a revoked key is rejected with the same 401 as a wrong key, so a probe can't enumerate live keys.
Rate-scoped
Each successful ingest updates lastUsedAt on the key so on-call can map the row back to the forwarder that calls it.
Per-tenant isolation lives in the bearer key's data plane, not in the credential itself. The cryptographic isolation, retention posture, and sector-pack guarantees behind that isolation are documented on the /trust surface.
thx_<8 url-safe>.<32 url-safe> # example (DO NOT use) thx_AbCdEfGhIjKl.0123456789abcdefABCDEFGHIJKLMNOP
prefix half
thx_AbCdEfGhIjKl
Always 12 chars (thx_ + 8 url-safe). The list at /app/settings/api shows this — it is safe to log.
secret half
0123456789abcdefABCDEFGHIJKLMNOP
32 url-safe chars. Scrypt-hashed before persistence; shown EXACTLY ONCE on creation. Treat as the credential.
▸ hash = scrypt(salt + secret) · constant-time compare at ingest · no raw secret on disk
Payload
Seven fields. Required: four. Three are optional metadata.
The endpoint expects a single JSON object per call. Each accepted event writes one AuditEvent row in the tenant that owns the key — no fan-out, no aggregation.
| Field | Type | Required | Notes |
|---|---|---|---|
| event_type | string | required | Stable identifier for the forwarded event — the kind your EDR/SIEM emits (e.g. edr.lateral_movement.detected). Stored verbatim on AuditEvent.type. ≤ 100 chars |
| severity | enum | required | Forwarder-classified severity. The ingest endpoint maps it onto the AuditEvent.severity enum. one of: info · low · medium · high · critical |
| entity | string | required | What the event is about — typically the host, asset, or session. Stored on AuditEvent.host. ≤ 200 chars |
| timestamp | ISO-8601 string | required | When the originating system emitted the event. Stored on AuditEvent.occurredAt (no implicit re-timestamp at ingest). UTC · e.g. 2026-08-12T14:32:11.000Z |
| source | string | optional | Free-form origin tag — the SIEM name, the EDR agent, the log shipper. ≤ 200 chars · stored verbatim |
| message | string | optional | Single-line summary shown in the analyst inbox rail. ≤ 2 000 chars |
| extra | object | optional | Anything else the forwarder wants to carry through — IOC fields, toolchain, parent event ids. Folded into AuditEvent.payload alongside ingest metadata. flat record of strings → values |
The call
One event. One POST. 201 if the key holds.
The body below is the exact shape Verodin / CrowdStrike / Wazuh bridges can normalise to. A successful call returns 201 with an audit_id the analyst side of the pane can use to reverse or escalate. A malformed body returns 400 with per-field reasons.
Need to ship in volume? Wrap a single forwarder in a bash while read loop, or batch on your SIEM side and POST each event individually — the endpoint is one event per call by design (one audit row per accepted event). Before you cut a release against the ingest path, check /status for current service health and the 90-day uptime strip.
curl -X POST 'https://threxen.app/api/telemetry/ingest' \
-H 'Authorization: Bearer thx_AbCdEfGhIjKl.<your-secret>' \
-H 'Content-Type: application/json' \
--data-raw '{ "event_type": "edr.lateral_movement.detected", "severity": "high", "entity": "host:east-web-04.corp", "timestamp": "2026-08-12T14:32:11.000Z", "source": "crowdstrike", "message": "Single-host SMB enumeration across three shares within 4s", "extra": { "technique_id": "T1021.002", "shares_touched": ["finance$", "legal$", "hr-readonly$"], "source_ip": "10.42.7.118" } }'Success response · HTTP 201
{
"audit_id": "<cuid>",
"accepted_at": "2026-08-12T14:32:11.412Z",
"tenant_slug": "<your-tenant-slug>"
}In the browser
Send the same call from inside this page.
Paste the API key you just minted and click POST /api/telemetry/ingest. The island fires the same body the curl above shows, plus the response body — including the audit_id — inline. No switching tabs to see whether your key held.
The response is parsed against the same zod contract the handler validates against, so a future drift between the page and the endpoint shows up here as an explicit error — not a silent mismatch.
Outgoing body
{
"event_type": "edr.lateral_movement.detected",
"severity": "high",
"entity": "host:east-web-04.corp",
"timestamp": "2026-08-12T14:32:11.000Z",
"source": "crowdstrike",
"message": "Single-host SMB enumeration across three shares within 4s",
"extra": {
"technique_id": "T1021.002",
"shares_touched": [
"finance$",
"legal$",
"hr-readonly$"
],
"source_ip": "10.42.7.118"
}
}▸ whitelisted · sandboxed · tenant-scoped
Awaiting send — the response will render here.
▸ end of docs · start of an account
Ready to mint a key and ship?
Two paths: signup → /app/settings/api → /docs/api → curl. The first creates a tenant; the second lets you revoke or read the key back later; the third is this page.
▸ key creation · tenant admins only · revocable from the same pane