06 · API · public
bearer-auth ingest · tenant-scoped

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.

Token shape
Two halves separated by a dot. Prefix is safe to log; secret half authenticates.
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.

FieldTypeRequiredNotes
event_typestringrequired

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

severityenumrequired

Forwarder-classified severity. The ingest endpoint maps it onto the AuditEvent.severity enum.

one of: info · low · medium · high · critical

entitystringrequired

What the event is about — typically the host, asset, or session. Stored on AuditEvent.host.

≤ 200 chars

timestampISO-8601 stringrequired

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

sourcestringoptional

Free-form origin tag — the SIEM name, the EDR agent, the log shipper.

≤ 200 chars · stored verbatim

messagestringoptional

Single-line summary shown in the analyst inbox rail.

≤ 2 000 chars

extraobjectoptional

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.

Single event — curl
Run verbatim with the key you minted. Replace the placeholder secret half with the value displayed in /app/settings/api.
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.

Try it with the key you just minted
We send the exact curl payload above — same body shape your forwarder will send — against your tenant's Bearer-authenticated endpoint and render the response inline.
in-page · 1 round trip

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.

Create an account Issue a key (sign-in required)

▸ key creation · tenant admins only · revocable from the same pane