POST /api/ingest
Authorization: Bearer <your-sdk-key>
Content-Type: multipart/form-data
The SDKs are preconfigured with the host and make this call for you. You only need the raw API when writing a client for a platform we do not ship an SDK for.
Authentication
Your app’s SDK key, as a bearer token. It is publishable — it grants report submission and nothing else, so shipping it inside a mobile binary or a web bundle is the intended use.
Body
multipart/form-data, with one JSON part and up to five binary parts:
| Part | Required | Notes |
|---|---|---|
envelope | yes | The envelope as JSON. May be gzip-compressed — detected from a content type of application/gzip or application/json+gzip, or from the gzip magic bytes. |
| attachment parts | no | Up to five, 25 MiB each. Each must be referenced from attachments[] by its partName. |
Every attachment is stream-hashed as it is read and checked against the
sha256 and byteLength you declared in the envelope. A mismatch fails the
whole request — the server will not store a blob it cannot verify.
curl -X POST https://api.everframe.dev/api/ingest \
-H "Authorization: Bearer $EVERFRAME_SDK_KEY" \
-F '[email protected];type=application/json' \
-F '[email protected];type=image/png'
Limits
| Limit | Value |
|---|---|
| Total request size | 25 MB |
| Decompressed envelope | 5 MB |
| Parts per request | 6 (envelope + 5 attachments) |
| Rate limit, per SDK key | 60 requests / minute |
| Rate limit, per source IP | 200 requests / minute |
The rate limits are deployment-dependent. They are applied only when the
service is configured with shared rate limiting enabled, which is off by
default; on a deployment without it, neither bucket rejects. When they are on,
both are applied after authentication, so the per-key bucket keys on the
validated key rather than a raw header. Write clients that honour 429
regardless — a limit you did not meet in staging can exist in production.
Response
200 on success — including a duplicate, which returns the original event:
{
"status": "received",
"eventId": "<uuid>",
"deliveryCount": 2,
"idempotent": false
}
| Field | What it is |
|---|---|
eventId | The stored event’s id. |
deliveryCount | How many subscribers this report is being fanned out to. |
idempotent | true if this reportId was already received, so it was de-duplicated rather than re-delivered. |
thread / device | Present only when replies are enabled for the app. See your SDK’s Two-way replies page. |
Idempotency
De-duplication keys on (app_id, reportId) and is not time-bounded: the event
record outlives its artifacts, so the same reportId is never fanned out twice
— an SDK retry after a flaky network is answered with 200 and the original
eventId. A client should therefore retry on 429 and 5xx without any
bookkeeping of its own, as long as it reuses the same reportId.
Errors
| Status | Error | Meaning |
|---|---|---|
400 | schema_validation_failed | The envelope did not match the schema, or carried an unknown top-level key. details lists the failing paths. |
400 | envelope_too_large | The decompressed envelope exceeded 5 MB. Note the status: this one is 400, not 413. |
400 | attachment_missing | The envelope references a partName that was not uploaded. |
400 | attachment_unexpected | A part was uploaded that the envelope does not reference. |
400 | envelope_part_missing | No envelope part in the multipart body. |
400 | envelope_invalid_json | The envelope part was not valid JSON, or could not be gunzipped. |
400 | no_parts | The request carried no parts at all. |
400 | sha256_mismatch / bytelength_mismatch | A part did not match the hash or length the envelope declared. |
400 | attachment_sha256_mismatch | An attachment’s bytes did not hash to its declared sha256. |
413 | attachment_too_large | One attachment exceeded 25 MiB. Note: 413, not 400. |
401 | invalid_sdk_key | Missing or invalid bearer token. |
403 | org_suspended | The organisation’s ingestion is suspended for exceeding its plan’s install limit. The body carries reason: "over_allowance" and since (a date). |
413 | payload_too_large | The whole request exceeded 25 MB. |
429 | rate_limit_exceeded | Too many requests. Honour the Retry-After header. |
Every 400 is terminal — fix the payload; retrying sends the same bytes. 429
and 5xx are safe to retry with backoff, reusing the same reportId.
403 is terminal too, and the SDKs treat it like an auth failure: no retry, so
a suspended org’s devices do not hammer the endpoint. Suspension happens on
every plan once an organisation has been over its monthly active install limit
for two complete calendar months and is over again; it lifts on upgrade, or
after a month finished within the limit. Reports rejected in between are not
stored. GET /api/config is not affected.
Match on the error string rather than on the status alone: an oversized
envelope and an oversized request are the same problem to a user and different
codes on the wire.