Errors
sinio.ai separates two kinds of failure, and the distinction drives everything on this page: a status code is about the call, a rejection code is about one event inside an otherwise successful call.
No status code should ever cost you an event. A 5xx is explicitly
replayable, and every other failure is something you fix rather than retry —
idempotency guarantees a replay creates nothing new.
Status codes
| Code | Meaning | What to do |
|---|---|---|
200 | Batch processed — read the counters | Read rejections |
400 | Malformed body, an unknown field, or more than 500 items | Fix the request |
401 | Key missing, invalid or revoked | Do not retry |
403 | Key lacks the required scope | Do not retry |
404 | Unknown resource | Fix the request |
413 | Request body over 10 MB | Split the batch |
429 | Rate limited | Back off and retry |
5xx | Temporarily unavailable | Replay identically |
The ingestion and reference-data routes — everything a key authenticates — are not rate-limited today, so they do not return 429; the code is listed so that your client handles it if limits are introduced.
A 200 is the interesting one: it means the batch was received, not that every event in it was accepted, and not that anything has been derived yet.
On /v1/events and the webhook URL, an unknown field — in the body, in an event or in its roles — is a 400 for the whole call (events.0.property foo should not exist for a key inside the first event), never dropped in silence. Only the content of payload is free.
Rejection codes
Rejections come back inside a 200, one entry per event, each with a code and a message. The rest of the batch is unaffected.
Ingesting events
- Name
invalid_occurred_at- Type
- code
- Description
The timestamp is not ISO 8601 with a UTC offset, written
YYYY-MM-DDThh:mm:ss(seconds required, fraction optional) followed byZor±hh:mm.2026-08-28T07:58:12and2026-08-28T07:58+02:00are rejected;2026-08-28T07:58:12+02:00and2026-08-28T05:58:12Zare accepted. A timestamp without an offset is not a moment, and guessing one would silently corrupt every duration derived from it. Message:occurred_at must be ISO 8601 with an offset, got: "2026-08-28T07:58:12".
- Name
payload_too_large- Type
- code
- Description
That event's payload exceeds 256 KB. The others in the batch still land.
- Name
missing_required_field- Type
- code
- Description
An event type's
interpretationmarks thesubjector theactoras required and the event did not carry it. Roles are optional by default: only an explicitrequired: trueproduces this.
- Name
trial_ended_quota- Type
- code
- Description
The organization's trial has ended without a subscription. Until it subscribes, sinio.ai keeps up to 5,000 events a month and analyses none; beyond that, events are rejected with this code until the next month or the subscription. A subscribed organization is never rejected for volume: its overage is counted. Retrying does not help.
Reference data
- Name
unknown_location- Type
- code
- Description
On
/v1/entities: thelocationdoes not exist yet. Declare it first withPOST /v1/locations.
- Name
unknown_parent_location- Type
- code
- Description
On
/v1/locations: theparentdoes not exist yet. Send parents before children.
- Name
invalid_validity_period- Type
- code
- Description
On
/v1/assignments:valid_fromorvalid_tois not an ISO 8601 date-time, orvalid_tois not aftervalid_from.
- Name
overlapping_assignment- Type
- code
- Description
On
/v1/assignments: this credential already has an assignment covering part of that window. Refused by a database exclusion constraint, so it holds under any race condition.
Two things that look like errors and are not
- Name
pending_classification- Type
- counter
- Description
The event's
typeis not known to the project yet. It is stored, with its payload intact, and derived once the type is given a meaning. This is how onboarding is meant to work — rejecting it would drop valid traffic and make a healthy integration look broken.
- Name
unmapped_event_type- Type
- finding
- Description
The type exists but no episode definition uses it yet. Accepted, stored, not derived, and reported as a
data_qualityfinding for someone to look at.
Neither is returned in rejections, and neither should be treated as a failure by your integration. pending_classification is a counter in the response; unmapped_event_type shows up in the console, not in the ingestion reply.
Retry policy
What to retry
429, 5xx, dropped connection → retry with exponential backoff + jitter
honour Retry-After if a response carries one
400, 401, 403, 404, 413 → do not retry, fix and resend
200 with rejections → do not retry the batch, fix those events
Retrying is always safe: within a project and a source, an event resent with the same source_event_id and content identical to any revision already stored is counted in duplicates and stored once — see Batching & idempotency. The official SDKs implement this policy already, and expose it as typed errors:
- Name
SinioAuthError- Type
- 401 / 403
- Description
Do not replay. Fix the key.
- Name
SinioRequestError- Type
- 400 / 404 / 413
- Description
Fix the request.
- Name
SinioRateLimitError- Type
- 429
- Description
Carries
retryAfterSeconds(retry_after_secondsin Python), from aRetry-Afterheader when there is one.
- Name
SinioServerError- Type
- 5xx
- Description
Replayable.
- Name
SinioConnectionError- Type
- network
- Description
Replayable.
- Name
SinioValidationError- Type
- local
- Description
Caught before the call — a timestamp with no offset, for instance.
PHP uses AuthException, RequestException, RateLimitException, ServerException, ConnectionException and ValidationException, all extending SinioException; the API's rejection code sits on $exception->errorCode, since $code belongs to \Exception.