Skip to documentation
InBrief

Event API reference

For your own developers: reporting what every endpoint actually returned

Internal analytics capability

On this page12

What this API reports

Send request outcomes from your servers to the private Internal page. This measures failures in real traffic, independently of scheduled uptime checks. For example, /health can return 200 while /v1/payments/charge fails one request in five.

Authentication

Generate a key: Dashboard → Account → Access, choosing Report telemetry only. The full key is shown exactly once, in that response, and never again.

Example
Authorization: Bearer ibk_ingest_<...>

Ingest keys are a separate scope from the ibk_live_ keys used by the incident API. An incident key can publish to your public page; an ingest key can submit telemetry, including Database Health reports when that separate feature is enabled. Use ingest keys on the application servers that send telemetry.

They are not interchangeable in either direction: an ingest key on /api/v1/incidents is rejected with 403, and so is an incident key here.

POST/api/v1/events

curl -X POST https://inbrief.sh/api/v1/events \
  -H "Authorization: Bearer ibk_ingest_<your key>" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    {
      "service": "orders",
      "endpoint": "/v1/orders/:id",
      "method": "GET",
      "status": 200,
      "ms": 31
    },
    {
      "service": "orders",
      "endpoint": "/v1/orders/:id",
      "method": "GET",
      "status": 503,
      "ms": 2140
    },
    {
      "service": "payments",
      "endpoint": "/v1/charge",
      "method": "POST",
      "status": 500
    }
  ]
}'
const response = await fetch('https://inbrief.sh/api/v1/events', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.INBRIEF_INGEST_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "events": [
      {
        "service": "orders",
        "endpoint": "/v1/orders/:id",
        "method": "GET",
        "status": 200,
        "ms": 31
      },
      {
        "service": "orders",
        "endpoint": "/v1/orders/:id",
        "method": "GET",
        "status": 503,
        "ms": 2140
      },
      {
        "service": "payments",
        "endpoint": "/v1/charge",
        "method": "POST",
        "status": 500
      }
    ]
  }),
});
if (!response.ok) throw new Error(`InBrief responded ${response.status}`);

A bare array is accepted too, if that is easier to produce.

FieldRequiredNotes
servicenoWhich of your back-end services served this (see below). Lower-cased. Omit it and events land in Unassigned. Aliases: app, component.
endpointyesThe route template (see below). Aliases: path, route.
methodnoUpper-cased. Defaults to GET.
statusyes100–599. Aliases: statusCode, status_code.
msnoResponse time in milliseconds, used to calculate average latency. The Internal page also shows p95 where histogram measurements are available. Alias: duration_ms.
messagenoShort description of what happened. Sampled, not logged (see below). Alias: error.
atnoISO-8601 timestamp. Defaults to arrival time.

Send every response, not only the failures. The successes are what make a rate possible: “412 errors” means nothing without “out of how many”, and the rate is the number anyone can act on.

4xx is recorded and visible, but never triggers an alert or opens an incident. A caller sending bad requests is not your service being down.

message is sampled, not stored per event. Only the most recent message on a failing response is kept, per endpoint per minute. Storing one string per event would undo the point of counting, while a sample answers the question you actually have in front of a red row: what did it say? It is truncated to 200 characters, control characters are stripped, and it is shown verbatim in your dashboard, so keep it generic: no customer data, no tokens, no full stack traces.

Send service if you run more than one

Set it to the name of the deployable that served the request: payments, auth, listings. It is optional.

Without it, every service you run shares one namespace. Three services that each expose GET /health become one row: their counts are added together, and the sampled error message can come from a different service than the failures did. Once merged, these records cannot identify which service failed.

Setting service keeps each service's counts, error rate and endpoints separate in the fleet map and table. A service failing 12% of its requests remains visible even when the overall error rate is 0.1%.

  • Lower-cased and trimmed. Payments and payments are the same service. This is the opposite of endpoint, which keeps its case because paths are case-sensitive.
  • A name, not an instance. Never a hostname, pod id, container id, or region; those are per-instance and would create a separate reporter per container. The per-page reporter allowance is 2,000; new reporters past that limit are refused.
  • A malformed service files the event under Unassigned rather than dropping it.
  • Max 60 characters.

You can omit service when all traffic belongs to one service.

Send route templates, not paths

Example
/v1/orders/:id     correct
/v1/orders/8837    wrong

Every framework knows the template it matched: req.route.path in Express, req.routeOptions.url in Fastify, Route::current()->uri() in Laravel, request.route_uri_pattern in Rails, request.resolver_match.route in Django.

Send raw paths and every id becomes its own endpoint. A day of traffic produces hundreds of thousands of one-request “endpoints”, the page becomes unreadable, and nothing can merge them back later: once split, the fact that those rows were one route is gone.

Query strings and fragments are stripped for you, and a trailing slash is normalised away. Case is preserved, because paths are case-sensitive.

Batching

Buffer events in memory and POST the batch every couple of seconds. Do not open a request per event.

Your code does no arithmetic. It appends to an array and flushes on a timer. All counting happens on our side, which is what lets us change what we measure without you upgrading anything.

The flush interval bounds the request count by time rather than by traffic: a server flushing every two seconds makes 30 requests a minute whether it handled 5 requests or 50,000.

Responses

202 Accepted:

Response
{
  "accepted": 3,
  "rejected": 0,
  "rows": 3,
  "statements": 3
}
  • accepted: events counted.
  • rejected: events dropped as malformed. Skipped individually, never failing the whole batch. Watch this while integrating; a non-zero value means something is wrong with what you send.
  • rows: database rows the batch collapsed into.
  • statements: writes reported by the selected event store. Use accepted and rejected to verify your integration.

If a cardinality guard fires, a warning field is added naming what was dropped. Nothing is discarded silently.

StatusMeaning
400Body was not JSON, or carried no event array.
401Missing, unknown, or revoked key.
403Key is not ingest-scoped, or internal analytics is unavailable.
413More than 1,000 events in one batch. Flush more often.
405Method other than POST.

POST/api/v2/events

The same events, sent by a reporter that names itself. Use this one when you want the Internal page to notice a process going quiet, and the one above when you only want the numbers.

curl -X POST https://inbrief.sh/api/v2/events \
  -H "Authorization: Bearer ibk_ingest_<your key>" \
  -H "Content-Type: application/json" \
  -d '{
  "reporter": {
    "reporterId": "orders-api-eu-1",
    "englishName": "Orders API (Frankfurt)",
    "region": "eu-central",
    "team": "payments",
    "expectedEverySeconds": 30
  },
  "events": [
    {
      "endpoint": "/v1/orders/:id",
      "method": "GET",
      "status": 200,
      "ms": 31
    },
    {
      "endpoint": "/v1/orders/:id",
      "method": "GET",
      "status": 503,
      "ms": 2140
    }
  ]
}'
const response = await fetch('https://inbrief.sh/api/v2/events', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.INBRIEF_INGEST_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reporter": {
      "reporterId": "orders-api-eu-1",
      "englishName": "Orders API (Frankfurt)",
      "region": "eu-central",
      "team": "payments",
      "expectedEverySeconds": 30
    },
    "events": [
      {
        "endpoint": "/v1/orders/:id",
        "method": "GET",
        "status": 200,
        "ms": 31
      },
      {
        "endpoint": "/v1/orders/:id",
        "method": "GET",
        "status": 503,
        "ms": 2140
      }
    ]
  }),
});
if (!response.ok) throw new Error(`InBrief responded ${response.status}`);
FieldRequiredNotes
reporter.reporterIdyesStable per process and unchanged across restarts. It is the identity the silence is measured against, so an id that changes on every deploy reads as one reporter dying and another appearing.
reporter.englishNameyesWhat an operator reads beside it.
reporter.regionnoLower-cased label, such as eu-central.
reporter.teamnoLower-cased label, such as payments.
reporter.expectedEverySecondsno15–300. How often you promise to call, heartbeat included.
eventsyesThe same event objects as above, and service is ignored: the reporter names itself. An empty array is a heartbeat, not an error.

Send this on your flush timer whether or not anything happened. A process with nothing to report still has something to say:

Response
{
  "reporter": {
    "reporterId": "orders-api-eu-1",
    "englishName": "Orders API (Frankfurt)",
    "region": "eu-central",
    "team": "payments",
    "expectedEverySeconds": 30
  },
  "events": []
}

202 Accepted answers with the v1 fields plus the two only a reporter batch has:

Response
{
  "accepted": 2,
  "rejected": 0,
  "rows": 2,
  "statements": 2,
  "heartbeat": true,
  "reporterId": "orders-api-eu-1"
}

Events older than ten minutes, or more than a minute in the future, are counted in rejected rather than failing the batch: a clock that is wrong should not cost you the rest of the events in the request.

Limits

Events per batch1,000
Distinct endpoints per batch200 , counted per service
Distinct endpoints per status page25,000
Distinct services/reporters per status page2,000
Endpoint length200 characters
Service name length60 characters
Message length200 characters
Traffic protection1,024 requests per key per minute; beyond that the route answers 429 until the minute turns. Buffered delivery sits far below it
Retention14 days at one-minute resolution in InBrief storage; yours to set in your own warehouse

Two rules for any integration

Never block a response on reporting. Buffer, and flush out of band.

Never throw on a failed flush. Telemetry that can take down the service it measures is worse than no telemetry. Dropping a batch is always better than failing a customer's request.

Where your events are stored

InBrief storage retains event history for 14 days at one-minute resolution. To use your own warehouse, open Dashboard → Internal → Storage.

Primary event storeTraffic written toTraffic read fromHistory retention
InBrief storage (default)our databaseour database14 days, fixed
Your warehouseyour warehouseyour warehousewhatever you set

Supported adapters are BigQuery, ClickHouse (Cloud or self-hosted) and Snowflake. Postgres, MySQL and Redshift are not supported destinations.

The selected store owns event history. InBrief retains reporter identity, region, team and reporting timestamps locally, plus encrypted connection credentials. Reporter metadata has no automatic expiry. Failed external writes are also buffered locally as aggregated batches. This operational data does not constitute a complete local copy of your warehouse history.

What changes when you switch

If the warehouse stops answering, the Internal page reports that it is unavailable. An accepted batch waiting for delivery returns 202 with a warning. That response confirms buffered acceptance, not arrival in the warehouse.

The local buffer holds at most 2,000 batches per status page and removes older batches above that cap. Automatic delivery stops after 50 failed attempts. Exhausted batches become eligible for deletion seven days after creation, not seven days after the last attempt. Switching back to InBrief storage discards pending external batches.

These limits can leave gaps in warehouse history. Restore the connection and check recent traffic before changing storage mode. Keep source records if you need to recover missing history; exhausted batches are not replayed automatically.

History does not move. What is already in InBrief storage stays there for its 14 days; your warehouse starts from the moment you switch. The page polls every 60 seconds instead of 15, because warehouse queries may be billed to your account. You create the table: the settings page shows the exact DDL, and we never create tables in your account.

A deduplication key accompanies every batch, but provider deduplication is bounded. A retry after a timeout can deliver a duplicate and inflate request or error counts. Bounded retries do not provide a guarantee that every batch will arrive.

Or keep ours and take a copy

If you like the live page as it is and only want longer retention, leave storage on InBrief and set a daily backup destination instead. Once a day we copy the previous day's rows into your warehouse, on your retention, through the same connectors. It runs out of band and never touches the path your servers report on.

With InBrief as the primary store, a warehouse outage does not interrupt the Internal page's traffic queries.

A note on Snowflake

Frequent writes and reads can keep a Snowflake warehouse running. Check your warehouse size, auto-suspend settings and Snowflake's warehouse guidance before using it as the primary store. A daily backup makes fewer requests, but its cost still depends on your configuration and data volume.

Credentials

Stored encrypted and never shown again once saved. Grant the narrowest thing that works: a service account with write access to that one table, not to the project. We verify the connection before saving anything, so a typo fails at the form rather than at 3am.

Where it shows up

Dashboard → Internal → HTTP heatmap combines a briefing, fleet map, filters and a service table. Select a service to inspect endpoints, requests, errors, error rate and latency. The view supports one-hour, six-hour and 24-hour windows, pinned investigations and copied links.

Any errors in the selected window classify a reporter as failing. Otherwise its reporting age determines late, silent or clean. Without a v2 heartbeat, reporting health is estimated from traffic. A sample below 100 requests is marked thin.

The Internal page guide explains those states, public-status gaps, unmonitored endpoints and alert rules. The page is private to your team. Publishing an incident requires a separate, authorised action.