HTTP API

Post events yourself. One endpoint, one body shape, no SDK.

The SDKs are a convenience. Anything that can make an HTTPS request can report, and the whole interface is one endpoint that takes one body shape.

Send a batch

One event, from a shell
Terminal
curl -X POST https://app.firstrun.app/v1/e \
  -H 'Content-Type: application/json' \
  -d '{
    "k": "fr_xxxxxxxxxxxxxxxx",
    "d": "install-a1b2c3",
    "r": { "service.version": "1.4.0", "os.type": "linux" },
    "e": [
      {
        "i": "0f8fad5b-d9cb-469f-a165-70867728950e",
        "t": 1788100000000,
        "n": "export_finished",
        "s": 9,
        "a": { "rows": 4210, "format": "csv" }
      }
    ]
  }'

No authorization header. The source key in k is the whole of it: it is public by necessity, it names a destination and it authorises nothing. Nothing you can read is reachable with one.

The body

Four fields at the top level, two of them required. The keys are one letter because the browser tag posts this from sendBeacon on a page that is closing, where bytes are the constraint. Everything else sends the same shape, so there is one format to implement rather than a compact one and a verbose one.

  • krequired. Your source key, fr_ followed by sixteen hex characters.
  • drequired. The distinct id: an anonymous id you generate once and persist. See below.
  • r — the resource. Attributes true of the whole client rather than of one event, such as service.version and os.type. They are merged under each event’s own attributes, so an event that sets the same key wins.
  • erequired. One to 500 events.

An event

  • irequired. A UUID you generate. It is the deduplication key: replaying a queue after a crash sends the same id and stores one row, so a client that cannot be sure a batch landed should simply send it again.
  • trequired. Milliseconds since the epoch, stamped by you, at the moment the thing happened. Not when you send it. An app that was offline on Tuesday and uploads on Friday is counted on Tuesday, and every chart in the product buckets on this field.
  • nrequired. The name. Any string matching [A-Za-z0-9][A-Za-z0-9_.-]{0,127}. There is no allowlist anywhere: exception, page_view and queue_depth_sampled are the same kind of thing and take the same path.
  • s — the severity, 1 to 24, on OpenTelemetry’s ladder. Absent means unclassified, which is not the same as INFO.
  • a — the attributes. A JSON object of anything you want to query by later.

Severity

Each band owns four numbers, and the first of each is the plain, unqualified one. Send 9 for an ordinary event and 17 for something that threw.

JavaScript
TRACE  1  2  3  4
DEBUG  5  6  7  8
INFO   9 10 11 12
WARN  13 14 15 16
ERROR 17 18 19 20
FATAL 21 22 23 24

The distinct id

Required on every batch, generated by you, and persisted to machine-local storage. It identifies an installation, not a person: on Windows that means %LOCALAPPDATA% and never %APPDATA%, because a roaming profile syncs between machines and one person on three of them would report as one install instead of three.

It is anonymous and it is scoped to this one source. Nothing here is ever joined to an id from another source. To count one person once across two of them, send user.id in r with the same value on both.

What comes back

JSON
{ "accepted": 1, "duplicates": 0, "dropped": 0 }

  • 202 — stored. duplicates counts events whose i had already arrived, and dropped counts ones rejected for their shape while the rest of the batch went through.
  • 400 — the body is not JSON, or not a batch this endpoint can read.
  • 404 — the source key does not resolve. A typo and a deleted source look the same from here, on purpose.
  • 413 — the body is over 1 MB.

Limits

  • 500 events per batch, 1 MB per body.
  • 64 attributes per map, nested at most 4 deep, keys up to 128 characters, strings up to 4096, arrays up to 128 items.
  • The distinct id is up to 512 characters, and so is any id-shaped attribute.

An event that breaks one of these is dropped and counted in dropped; the rest of the batch is stored. A batch that breaks one is refused whole.

Write it like a client would

Nothing above requires the discipline the SDKs have, and everything you build on it should still have it. Queue and batch rather than posting per event, bound the queue and drop the oldest when it is full, never block a path a human is waiting on, and never let a failure here reach the program you are instrumenting. Losing telemetry is always the right trade against affecting your own software.