Skip to main content
Submits an indicator of compromise for analysis. The response is a Server-Sent Events stream (text/event-stream) that emits metadata, AI model information, streamed markdown content, and a final done signal.
GET /api/ctai

Query parameters

ioc
string
required
The indicator of compromise to analyze. Supported types:
  • IPv4 — e.g., 1.2.3.4
  • IPv6 — e.g., 2001:4860:4860::8888
  • Domain — e.g., malicious-domain.example
  • MD5 hash — 32 hex characters
  • SHA1 hash — 40 hex characters
  • SHA256 hash — 64 hex characters
If the value does not match any of these formats, the API returns a 400 error.
model
string
The AI model ID to use for analysis. Must be one of the allowed model IDs listed below. If omitted or invalid, the server defaults to openrouter/free.
Model IDLabel
openrouter/autoDefault — OpenRouter (Auto)
openrouter/freeOpenRouter (Free) — default
liquid/lfm-2.5-1.2b-instruct-20260120:freeLiquidAI: LFM2.5-1.2B-Instruct (Free)
stepfun/step-3.5-flash:freeStepFun: Step 3.5 Flash (Free)
google/gemma-3-4b-it:freeGoogle: Gemma 3 4B (Free)

Request headers

X-OpenRouter-Key
string
Your OpenRouter API key. Used to call the AI model. Falls back to the server’s OPENROUTER_API_KEY environment variable if omitted. If neither is present, the AI analysis step fails and an error SSE event is emitted with errorType: "invalid_api_key".
X-VT-Key
string
Your VirusTotal API key. Used to enrich IP, domain, and hash IoCs. Optional — if omitted, the server falls back to its own key, or skips VirusTotal and emits a warning.
X-AbuseIPDB-Key
string
Your AbuseIPDB API key. Used to enrich IP address IoCs. Optional.
X-Polyswarm-Key
string
Your PolySwarm API key. Used to enrich hash IoCs. Optional.

Response

Status: 200 OK
Content-Type: text/event-stream
Connection: keep-alive
The response body is a stream of SSE events. The normal sequence is:
meta → model → chunk (×N) → done
If a non-fatal source fails, warnings are included in the meta event and the stream continues. If a fatal error occurs mid-stream, an error event is emitted instead of done. See SSE Events for the full schema of each event type.

Error responses

These errors are returned as application/json before the stream opens.
The ioc query parameter was not included in the request.
{ "error": "Falta el parámetro de IoC" }
The value of ioc did not match any supported format (IPv4, IPv6, domain, MD5, SHA1, SHA256).
{ "error": "Tipo de IoC desconocido" }
The requesting IP has exceeded the allowed request rate. The retryAfterSeconds field tells you how long to wait before retrying. See Rate Limiting.
{ "error": "Too many requests", "retryAfterSeconds": 12 }
An unexpected error occurred before or during stream initialization — for example, all CTI sources failed.
{
  "error": "No se pudo completar la consulta de fuentes del IoC.",
  "stage": "ioc",
  "errorType": "unknown"
}

Examples

curl "http://localhost:4321/api/ctai?ioc=1.2.3.4"

Specifying a model

curl "http://localhost:4321/api/ctai?ioc=1.2.3.4&model=openrouter/auto"

Using your own API keys

curl "http://localhost:4321/api/ctai?ioc=1.2.3.4" \
  -H "X-OpenRouter-Key: sk-or-your-key-here" \
  -H "X-VT-Key: your-virustotal-key" \
  -H "X-AbuseIPDB-Key: your-abuseipdb-key"

Parsing the stream

The response is a raw SSE stream. Here is a minimal example of consuming it in a browser or Node.js environment:
const response = await fetch(
  "http://localhost:4321/api/ctai?ioc=1.2.3.4&model=openrouter/free",
  {
    headers: {
      "X-OpenRouter-Key": "sk-or-your-key-here"
    }
  }
);

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const parts = buffer.split("\n\n");
  buffer = parts.pop() ?? "";

  for (const part of parts) {
    const eventLine = part.split("\n").find((l) => l.startsWith("event:"));
    const dataLine = part.split("\n").find((l) => l.startsWith("data:"));
    if (!eventLine || !dataLine) continue;

    const event = eventLine.slice("event:".length).trim();
    const data = JSON.parse(dataLine.slice("data:".length).trim());

    if (event === "meta") console.log("IoC:", data.ioc, "| Type:", data.type);
    if (event === "chunk") process.stdout.write(data.content);
    if (event === "done") console.log("\n[Stream complete]");
    if (event === "error") console.error("Error:", data.error);
  }
}
The AI response content (chunk events) is Markdown-formatted text in Spanish. Use a Markdown renderer such as marked with DOMPurify sanitization before displaying it in a browser.

Build docs developers (and LLMs) love