Skip to content
Everframe Docs
Documentation

Ingest API

The SDKs talk to one endpoint. If you are building a custom client, this is the contract.

Updated

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:

PartRequiredNotes
envelopeyesThe 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 partsnoUp 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

LimitValue
Total request size25 MB
Decompressed envelope5 MB
Parts per request6 (envelope + 5 attachments)
Rate limit, per SDK key60 requests / minute
Rate limit, per source IP200 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
}
FieldWhat it is
eventIdThe stored event’s id.
deliveryCountHow many subscribers this report is being fanned out to.
idempotenttrue if this reportId was already received, so it was de-duplicated rather than re-delivered.
thread / devicePresent 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

StatusErrorMeaning
400schema_validation_failedThe envelope did not match the schema, or carried an unknown top-level key. details lists the failing paths.
400envelope_too_largeThe decompressed envelope exceeded 5 MB. Note the status: this one is 400, not 413.
400attachment_missingThe envelope references a partName that was not uploaded.
400attachment_unexpectedA part was uploaded that the envelope does not reference.
400envelope_part_missingNo envelope part in the multipart body.
400envelope_invalid_jsonThe envelope part was not valid JSON, or could not be gunzipped.
400no_partsThe request carried no parts at all.
400sha256_mismatch / bytelength_mismatchA part did not match the hash or length the envelope declared.
400attachment_sha256_mismatchAn attachment’s bytes did not hash to its declared sha256.
413attachment_too_largeOne attachment exceeded 25 MiB. Note: 413, not 400.
401invalid_sdk_keyMissing or invalid bearer token.
403org_suspendedThe organisation’s ingestion is suspended for exceeding its plan’s install limit. The body carries reason: "over_allowance" and since (a date).
413payload_too_largeThe whole request exceeded 25 MB.
429rate_limit_exceededToo 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.