Skip to main content

Sending events with the HTTP Event Collector

The exact endpoint, header, payload shapes, limits and every response code for posting logs over HTTPS — with a request you can copy and run.

Written for whoever runs IT9 min readUpdated

The HTTP Event Collector is the one log source you can set up entirely by yourself, in about five minutes, with nothing installed anywhere. You issue an ingest token, you post JSON or plain text to one URL, and the response tells you how many events were accepted. It is deliberately compatible with the Splunk collector convention, so most existing log shippers can be pointed at it with a URL and token change and nothing else.

This article is written to be copied from. Every string in it is exact.

The endpoint

The one and only collector path
POST /api/v1/siem/hec

This route sits outside the usual tenant path guard on purpose — it is listed in the API's own source among the routes that are public by design because they are token-authenticated ingest. There is no organisation identifier in the URL. Your organisation is determined entirely by the token, which is why the token is the only thing standing between an attacker and your security telemetry.

Anything other than POST is refused:

Wrong method
HTTP/1.1 405 Method Not Allowed

method not allowed

Authentication

One header. Two accepted forms, because Splunk-compatible shippers send the first and everything else sends the second. The prefix is matched case-insensitively, so SPLUNK and splunk both work.

Either of these is valid
Authorization: Splunk zqi_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Authorization: Bearer zqi_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

There is no custom header. X-Qorionix-Ingest-Token and similar do not exist and never did — if you find one in a script, someone invented it. See Ingest tokens for how to issue one.

A real request

This is correct and complete. Replace the host with the API host for your deployment and the token with your own.

Two events as a JSON array
curl -sS -i \
  -X POST 'https://<your-api-host>/api/v1/siem/hec' \
  -H 'Authorization: Splunk zqi_YOUR_TOKEN_HERE' \
  -H 'Content-Type: application/json' \
  --data '[
    {"timestamp":"2026-09-01T09:14:22Z","user":"[email protected]",
     "action":"login","outcome":"failure","source_ip":"203.0.113.44",
     "host":"vpn-gw-01"},
    {"timestamp":"2026-09-01T09:14:31Z","user":"[email protected]",
     "action":"login","outcome":"success","source_ip":"203.0.113.44",
     "host":"vpn-gw-01"}
  ]'
The response
HTTP/1.1 202 Accepted
Content-Type: application/json

{"accepted":2,"text":"Success"}

accepted is the number of records the payload was split into, not a promise about parsing. If you sent two records and it says one, your payload shape is not what you thought — which is a more useful thing to learn from a counter than from a search that returns nothing an hour later.

The batch endpoint, honestly

There is no separate batch endpoint. Some collectors have one; this one does not. Batching is by payload shape on the same URL: send an array, or a concatenated stream of objects, or many newline-delimited lines, and every record in the body is one event.

Payload shapes

The Content-Type header decides how the body is split. Two families are accepted.

JSON — Content-Type: application/json, or absent

An empty Content-Type is treated as JSON, which is convenient and occasionally surprising. Four shapes are accepted:

  • An array — [ {...}, {...} ]. Each element is one event. This is the shape to prefer.
  • A concatenated object stream — {...}{...} with no separator. Each object is one event. This is what some shippers emit and it is accepted as-is.
  • A single object — {...}. One event.
  • A Splunk envelope — {"event": {...}}. The envelope is unwrapped and the inner object becomes the record. This is what makes existing Splunk shippers work unchanged.

Text — Content-Type: text/plain, or any text/*

Newline-delimited raw log lines, one event per line. Blank lines are skipped rather than stored as empty events. Use this for anything you are tailing off disk.

Rejected bodies

The two 400 responses you can cause with a body
HTTP/1.1 400 Bad Request

unsupported Content-Type: application/x-www-form-urlencoded

---

HTTP/1.1 400 Bad Request

empty payload

Malformed JSON also returns 400, with the decoder's own message as the body. That message is a Go standard library string and is not a stable contract, so match on the status code rather than the text.

Size and rate limits

Every limit that applies to this endpoint, with the honest caveat on each. Read the rate limit row twice.
LimitValueWhat happens at the edge
Maximum request bodyNo explicit capThere is no configured body size limit on this endpoint. That is not an invitation to post a gigabyte — infrastructure in front of the API may impose its own — but nothing in the handler will reject a large body for being large.
Maximum single line, text mode1 MiBThe line scanner starts at 64 KiB and grows to a 1 MiB maximum. A single line longer than that fails the read and the request returns 400.
Rate, steady1,000 events per second per tokenEnforced as a token bucket, per token, per API process. The bucket starts full.
Rate, burst2,000 eventsThe bucket depth. A burst above it is throttled.

The second caveat on the rate limit is arithmetic. The limiter holds its state in the memory of a single API process, so a deployment running more than one replica multiplies the effective ceiling by the number of replicas. A shared implementation that would make the limit exact across replicas exists in the codebase and is not wired up. Treat 1,000 events per second as a floor you can rely on, not as a hard ceiling that will protect you from a leaked token.

When storage fails

Two 503 responses, and they are the most important thing on this page after the endpoint itself.

Storage and queue failures
HTTP/1.1 503 Service Unavailable

queue unavailable

---

HTTP/1.1 503 Service Unavailable

ingest sink unavailable

These exist because of a fixed defect worth knowing about. An earlier version discarded the storage error and answered 202 anyway, so a collector that persisted not one single row was indistinguishable from a working one. The comment in the code is blunt about the consequence: that is how ingestion works stayed believed for as long as it did. A 202 from this endpoint now means the events reached storage. A retry on 503 is safe and correct — duplicate submissions are deduplicated where the record carries an identifier.

Which parser runs, and what the source is called

The parser is chosen by the token, not by the request. A token issued with parser_kind of cisco_asa will run the Cisco parser on everything posted with it, so use one token per format rather than one token for everything.

If the parser succeeds but leaves the source name empty, the handler stamps the parser name in. That detail matters more than it sounds: the normalisation layer chooses its mapping from the source field, so an empty source is the difference between a fully mapped event and a generic one. If no parser matched at all, the line is stored raw with a source name of hec for the generic parsers, or the parser's own name otherwise.

One incidental behaviour: the address recorded against your token prefers the first hop in the X-Forwarded-For header, because the API sits behind a load balancer in every deployment. If your collector is behind a proxy that sets that header, the address you see on the token is the client's, not the proxy's.

What to read next

  1. Ingest tokens — issuing, rotating, revoking, and what to do if one leaks.
  2. Sending events with OTLP — the alternative endpoint, and when it is the right one.
  3. How your logs are normalised — what happens to your JSON after the 202.
  4. Searching your logs — confirming your events actually landed.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in logs and detection