TrailStack Docs
Guides

Ingestion

Event format, batching, schema evolution, and error handling.

Everything you need to send events from your application to TrailStack.

Endpoint

POST https://api.trailstack.io/v1/Spaces/AppendRecords
ParameterRequiredDefaultDescription
spaceNameYesTarget Space name
fireAndForgetNotrueReturn immediately or wait for confirmation
trackIpAddressNofalseCapture client IP as the ip field
trackCountryNofalseResolve country code from IP as the country field

Content-Type: text/plain

Authorization: Bearer <your-inject-token>

Event format

Each event is a JSON object. The event field is required — it determines the view name. {"event": "page_view"} creates a view called evt_page_view.

{
  "event": "page_view",
  "properties": {
    "path": "/pricing",
    "duration_ms": 3200,
    "authenticated": true
  }
}
FieldRequiredTypeDescription
eventYesstringEvent name. Creates a view called evt_<name> in ClickHouse.
properties or attrsNoobjectEvent attributes. Each key becomes a typed column.
references or refsNoobjectReference-type attributes (foreign keys to entities).
timeNonumber or stringUnix seconds or ISO-8601. Defaults to server insertion time.

Field naming rules: lowercase letters, numbers, and underscores. Must start with a letter. Pattern: ^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$

Batching

Send multiple events in one request using newline-delimited JSON (NDJSON). Each line is one event:

{"event": "page_view", "properties": {"path": "/pricing", "duration_ms": 3200}}
{"event": "page_view", "properties": {"path": "/docs", "duration_ms": 1500}}
{"event": "signup", "properties": {"plan": "free", "campaign": "google"}}

All lines in a request go to the same Space. Each line is parsed independently — a malformed line does not block valid lines.

Response

200 OK

{"appendedLines": 3, "appendedBytes": 245}

When fireAndForget=true (default), counters return 0 because the server enqueues the payload and responds immediately. Set fireAndForget=false to get actual counts.

402 Payment Required — storage limit reached. The response includes your current usage and a link to upgrade. Your data stays readable and queryable.

404 Not Found — Space does not exist. Check the spaceName parameter.

400 Bad Request — invalid request format.

fireAndForget

  • true (default): the server writes your payload to a queue and returns immediately. Fastest option. Use this in production when you do not need confirmation per request.
  • false: the server waits until the payload is appended and returns actual line and byte counts. Use this when testing or when you need delivery confirmation.

Schema evolution

TrailStack detects types from your JSON and creates ClickHouse columns automatically. No migrations, no schema definitions.

Detected types:

JSON valueClickHouse type
"hello"String
42 or 3.14Float64
true / falseBool
nullIgnored (no column created)

New fields: when you send an event with a field TrailStack has not seen before, it adds the column automatically. Your next query sees the new column.

Mixed types: if you send the same field with different types across events (for example, "count": 42 in one event and "count": "hello" in another), both values are stored in separate typed columns. There is no data loss and no coercion.

Removing fields: if you stop sending a field, the column stays. Existing data is not affected. The column contains null for events that do not include it.

Unsupported values: objects and arrays inside properties are not supported. The entire event becomes a garbage row — the raw payload is preserved with an error message in the all_errors view.

What creates garbage rows:

  • Malformed JSON
  • Unknown top-level fields (allowed: event, properties/attrs, references/refs, time, nonce, deleted)
  • Objects or arrays inside properties
  • Invalid field names (must match ^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$)
  • Duplicate field names in the same event

Auto-detected fields

When trackIpAddress and trackCountry are enabled (both default to false), TrailStack adds two fields to every event:

FieldValue
ipClient IP address
countryTwo-letter country code resolved from the IP

Set trackIpAddress=true and trackCountry=true in the query string to enable.

On this page