Every report — from a React web app, an iPhone, or a Roku box — arrives in the same shape. That is the whole point: your triage prompts and integrations are written once.
{
"protocolVersion": "1.0",
"reportId": "<uuid>",
"submittedAt": "2026-06-15T12:00:00.000Z",
"sdk": { },
"reporter": { },
"captures": { },
"captureControl": { },
"payload": { },
"context": { },
"attachments": []
}
Top level
| Field | What it is |
|---|---|
protocolVersion | Schema version. Currently "1.0". |
reportId | UUID. Stable across retries — this is the idempotency key. |
submittedAt | ISO-8601 submission timestamp. |
source | How the report was filed: manual, crash or error. Optional — absent means manual. |
sdk | name (everframe-react, everframe-web, everframe-react-native, everframe-ios, everframe-android), version, platform (web, ios, android, tvos, androidtv, tizen, webos) and formFactor (phone, tablet, desktop, tv). |
reporter | What the user typed: title and description, plus optional user. Protocol caps are 50 / 600 characters, but ingest clips the description to 400 — see Errors & limits. |
captures | Booleans: which capture types were enabled for this report. |
captureControl | What was actually included / excluded, plus degradedReason when a capture was enabled but could not run. |
payload | The captured artifacts. |
context | App, device and route. |
attachments | References to the binary parts. |
captures and captureControl are not redundant. The first is intent, the
second is outcome — a receiver can tell “replay was off” apart from “replay was
on but the buffer was empty”, which is the difference between a config problem
and a bug.
payload
| Field | What it is |
|---|---|
breadcrumbs | The action timeline. Up to 128 kind-tagged entries on one epoch-ms clock, shared with the replay. |
networkBodies | Request/response bodies, joined to network crumbs by ref === crumb.data.reqId. Off unless enabled. |
resources | CPU/memory samples from the seconds before the report, one entry per sample tick. Off unless enabled — see Server config. cpu is absent on web (no browser API exposes it). |
annotations | The user’s drawings on the screenshot. |
redactions | Regions the user blurred. |
crash | Exception details. Present only when the top-level source is crash or error. |
extra | Free-form metadata you attached — a string, or an object the SDK serialises. ≤ 16 384 chars (16 KiB). |
logs / network | Deprecated. Derived from breadcrumbs; read breadcrumbs instead. |
Breadcrumbs
{
"t": 1765800000123,
"seq": 42,
"kind": "network",
"level": "error",
"message": "POST /api/cart",
"data": {
"reqId": 17,
"status": 500
}
}
t (epoch ms) and seq (a per-session tiebreaker) are required and
numeric. kind is one of navigation, tap, console, network,
lifecycle, error, custom; level, when present, is debug, info,
warn or error. message is capped at 2 048 characters; truncated: true
marks a message that was clipped.
data.reqId on a network crumb is a number, and it is what
payload.networkBodies[].ref joins to.
The chain is capped at 128 entries — 100 by config plus headroom for the
per-kind trim markers, which tell you that entries were dropped rather than
silently losing them. A trim marker is an ordinary crumb of the trimmed kind
whose data.droppedCount says how many were dropped.
context
{
"app": {
"name": "Acme",
"version": "2.4.0",
"build": "1182"
},
"device": {
"os": "iOS",
"osVersion": "18.2",
"model": "iPhone15,3",
"screenSize": {
"width": 393,
"height": 852
},
"pixelRatio": 3,
"locale": "en-GB",
"timezone": "Europe/Vilnius"
},
"route": "/checkout"
}
device.userAgent is present on web only — native SDKs omit it.
attachments
Binary parts live outside the JSON and are referenced from it:
{
"partName": "screenshot",
"kind": "screenshot",
"contentType": "image/png",
"byteLength": 84213,
"sha256": "<hex>",
"width": 1170,
"height": 2532
}
kind is screenshot, annotated-screenshot, video, audio,
session-replay or other. width, height and durationMs are optional.
Replay attachments carry a format discriminator — rrweb on web,
everframe-vtree-v1 natively — so a player knows what it is holding without
sniffing bytes.
Always verify sha256 before trusting an attachment. The ingest API
verifies it on the way in and will reject a mismatch, but a receiver that writes
attachments to storage should check it too.
In a webhook delivery each reference also carries a presigned url and
expiresAt, or a marker explaining why it does not — see
Webhooks.
Compatibility
The envelope is additive. New optional fields appear without a version bump, so
a receiver must ignore unknown fields rather than reject them — every object
in the schema is a passthrough. A field is only ever removed behind a protocol
version change, and deprecated fields are kept alive for a full deprecation
window first, as logs and network are now.
The one place that is strict is the envelope root at ingest: an unknown
top-level key is rejected with schema_validation_failed. Nested objects stay
open, so a custom client may add fields inside payload or context, but not
beside them.