Ingest formats

Errorbird offers four write endpoints. All of them use the same X-API-KEY authentication, the same quota and the same PII scrubbing; they differ only in the body format.

Endpoint Body Use
POST /api/v1/ingest A single CLEF event (JSON) Manual testing, low volume
POST /api/v1/ingest/batch NDJSON (one event per line), gzip supported Production; Serilog/Seq sinks
POST /api/v1/ingest/otlp OTLP/HTTP JSON logs OpenTelemetry Collector
POST /api/v1/ingest/browser Browser error (JSON) @errorbird/browser; see Browser errors

All of them return HTTP 202 on success, meaning "queued". Waiting for the write inside the request would tie your application's response time to Errorbird's database speed; that is why the acceptance criterion is "202 within 50 ms".

CLEF fields

CLEF (Compact Log Event Format) is the format Seq uses. Errorbird recognizes these fields:

Field Meaning
@t Timestamp (ISO 8601). If missing, the time the server received the event is used.
@l Level: Verbose, Debug, Information, Warning, Error, Fatal
@mt Message template (User {UserId} not found)
@m Rendered message
@x Exception text / stack trace
@r Rendered values of the template
@i Event type id

Template properties (such as UserId) are stored as jsonb in Metadata and are searchable. Errorbird also reads release, environment, serverName and stackTrace as top-level fields — they answer "which release introduced this error?".

Migrating from Serilog

In your existing Serilog.Sinks.Seq configuration, only the address and the key change, with no code changes:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Seq(
        serverUrl: "https://api.errorbird.com/api/v1/ingest",
        apiKey: "ebrd_secret_…")
    .CreateLogger();

The sink sends events in batches as NDJSON, and Errorbird's batch endpoint accepts exactly that. Client-side buffering and retries are the sink's own behavior and keep working as they are.

Batch sending

gzip -c events.ndjson | curl -X POST https://api.errorbird.com/api/v1/ingest/batch \
  -H "X-API-KEY: ebrd_secret_…" \
  -H "content-type: application/x-ndjson" \
  -H "content-encoding: gzip" \
  --data-binary @-

A single request can carry at most 5,000 events; a larger body returns 400. The limit protects memory: if one request could open an unbounded buffer on the server, the ingest pipeline would be at the mercy of a single client.

OTLP

curl -X POST https://api.errorbird.com/api/v1/ingest/otlp \
  -H "X-API-KEY: ebrd_secret_…" \
  -H "content-type: application/json" \
  -d @otlp-logs.json

The resourceLogs[].scopeLogs[].logRecords[] structure is read; severityText becomes the level, body.stringValue the message, and attributes go to Metadata.

Error codes

Code Meaning What to do
401 The key is invalid or revoked Check the key
400 The body could not be parsed or the batch limit was exceeded Fix the body
429 The monthly quota is used up or the rate limit was exceeded Wait for the Retry-After header
503 Circuit breaker open (the queue cannot be written) Keep the events in a client buffer and retry

The write rate limit is 2,000 events per second (token bucket, per key) and is separate from the read pool: read traffic does not slow down writes, and write traffic does not slow down reads.

Do not drop events on 429 or 503. Both are temporary; buffer on the client and resend with exponential backoff.

How is the fingerprint calculated?

Grouping the same root error under a single Issue relies on a SHA-256 digest of the normalized message and stack trace:

  1. Variables in the message are replaced with placeholders. GUIDs, emails, URLs, file paths, timestamps, IPs, hex values, quoted text and numbers are replaced in that order. Cart 9f3c… not found for user 4821 and Cart a1b2… not found for user 77 produce the same signature.
  2. The exception type is extracted. A prefix such as System.NullReferenceException: message is split off and enters the signature as a separate component.
  3. The first 5 stack frames are used. Deeper frames vary with the calling code and would scatter the same error across different groups.
  4. Line numbers are dropped and paths are made machine-independent. Otherwise a one-line edit or a different build machine would create a new Issue.

.NET, Node.js, Java and browser (Chrome/Edge and Firefox/Safari) stack trace formats are recognized separately. In browser frames the host and the query string are dropped as well: the same bundle served from a CDN or with ?v=3 produces the same signature. For minified JavaScript, the signature is calculated from the stack mapped back to the original code with a source map. When the same root error is sent 500 times with different line numbers and different variable values, 500 raw rows are written to LogEvents, but only one row is created in Issues, with a totalCount of 500.

Quota and retention

The quota is monthly and depends on the plan (Hobby 5,000, Pro 100,000, Business 1,000,000 events). When it runs out, ingest returns 429; the counter resets at the start of the month.

Retention also depends on the plan (3 / 15 / 30 / 90+ days). Expired data is dropped at the chunk level in TimescaleDB with drop_chunks; there is no row-by-row deletion.