Events ingestion
Events are the entry point for action data. One synchronous POST: the event persists as a fact and is evaluated inline before the response returns. The response itself is narrow (ids, status, and rule-granted rewards); the full downstream effect shows up in participant state, webhooks, and the SSE stream. This page covers the two endpoints, idempotency, batching, and retry behavior.
Key takeaways
Quick read- Two endpoints, both synchronous: POST /gamify/events (single) and POST /gamify/events/batch (up to 100). Processing completes before the response returns.
- Idempotency-Key header makes every retry safe. Keyless requests get a server-synthesized key, so accidental retries never double-credit.
- Auth is dual-path: X-API-Key from your backend (events:write scope, included in the mint default), or a participant JWT from the browser, pinned to its own participant_id.
- Per-participant rate limit: 100 events/minute by default. Batched events count individually; over-limit items are marked rate_limited, not the whole batch.
- One event per user action. The rules engine fans it out; you almost never call points or badges endpoints directly from an event flow.
Endpoints
Two ways to send events
POST /gamify/events
Synchronous single event. Persists the fact, evaluates rules inline, and returns event_id, fact_id, status, plus rewards_granted (rules-engine action outcomes only). The default for interactive flows.
POST /gamify/events/batch
Synchronous batch of 1 to 100 events. Each event processes independently with per-item status; one bad event does not fail the request.
Single event
The default path
One event, one POST, processed before the response returns. The body carries ids, status, and any rule-granted rewards; for the full picture of what changed (challenge progress, tier moves), read participant state or listen on webhooks/SSE.
curl -X POST https://api.bricqs.co/api/v1/gamify/events \
-H "X-API-Key: bq_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: user_42:order_4271" \
-d '{
"participant_id": "user_42",
"event_name": "purchase_completed",
"properties": {
"amount": 1250,
"currency": "INR",
"category": "skincare"
},
"timestamp": "2026-07-19T10:42:00Z"
}'
# response (synchronous; rules already evaluated)
HTTP/1.1 200 OK
{
"event_id": "01K0...",
"fact_id": "01K0...",
"status": "processed",
"rewards_granted": []
}From a browser or mobile app, send a participant JWT in the Authorization header instead of X-API-Key; the token is pinned to its own participant_id. Field reference: /docs/gamification-api/events.
Idempotency
Retries are safe by default, safer with a key
Networks fail and clients retry. Two layers protect you: the server synthesizes a dedupe key when you send none, and an explicit Idempotency-Key header gives you full control for offline replay.
Header: Idempotency-Key
Format: 1 to 255 chars matching [A-Za-z0-9_-:.]
Recommended shape: <participant>:<subject>[:<period>]
user_42:order_4271 one-shot purchase
user_42:daily_login:2026-07-19 daily action (one per day)
user_42:quiz_fit_basics:attempt_3 distinct attempts, distinct keys
Behavior:
Same key again = the stored event's ids come back with
status: "duplicate". No new fact, no double-credit.
rewards_granted is empty on duplicates (rule
outcomes are not replayed in the response).
No key supplied = the server synthesizes one from the event's
identity (synth: prefix). Accidental retries of
the identical request deduplicate; deliberate
re-sends with a changed payload do not.
Header AND body key = must match, else 400 IDEMPOTENCY_KEY_MISMATCH.
Offline replay (mobile): queue events locally WITH explicit keys, then
replay the queue on reconnect. Every already-delivered event dedupes and
comes back with status: "duplicate".Batch
Up to 100 events in one POST
Use batch for reconnect replays and server-side fan-out. Each event carries its own idempotency_key in the body (the request-level header is not applied to batches).
curl -X POST https://api.bricqs.co/api/v1/gamify/events/batch \
-H "X-API-Key: bq_live_..." \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"participant_id": "user_42",
"event_name": "lesson_completed",
"properties": { "lesson_id": "l_001" },
"idempotency_key": "user_42:l_001"
},
{
"participant_id": "user_91",
"event_name": "lesson_completed",
"properties": { "lesson_id": "l_002" },
"idempotency_key": "user_91:l_002"
}
]
}'
# response: per-item results in submission order
HTTP/1.1 200 OK
{
"processed": 2,
"failed": 0,
"results": [
{ "event_id": "01K0...", "fact_id": "01K0...", "status": "processed", "rewards_granted": [] },
{ "event_id": "01K0...", "fact_id": "01K0...", "status": "processed", "rewards_granted": [] }
]
}Per-item statuses you must handle: processed, error (schema/processing failure), rate_limited (that participant is over their per-minute cap), and forbidden (JWT callers only: the event's participant_id does not match the token). The rest of the batch always continues.
Retry policy
What to do when ingestion fails
Status codes:
200 OK Processed, or status "duplicate" for a repeated key.
400 Bad Request IDEMPOTENCY_KEY_INVALID / IDEMPOTENCY_KEY_MISMATCH.
Do NOT retry; fix the key.
401 AUTH_UNAUTHENTICATED / AUTH_API_KEY_INVALID.
Do NOT retry; fix auth.
403 AUTH_FORBIDDEN. API key lacks events:write, or a JWT
tried to emit for another participant. Fix scopes.
422 Schema validation (empty batch, >100 events, bad
field). Do NOT retry; fix the payload.
429 RATE_LIMIT_PARTICIPANT_EXCEEDED (or tenant/IP tier).
Retry after the Retry-After header value.
500/502/503/504 Server error. Retry with exponential backoff and
jitter; your idempotency key makes this safe.
Every error body uses the standard envelope:
{ "error": { "type": "...", "code": "...", "message": "...",
"request_id": "req_..." } }
Log request_id; support can trace any request by it.Common mistakes
What goes wrong on first integration
Treating ingestion as fire-and-forget async, or expecting challenge/tier details in the ingestion response.
Ingestion is synchronous, but the response carries only ids, status, and rule-granted rewards. For celebration UI, refresh state (GET /gamify/state/{participant_id} or the SDK hooks) or subscribe to the SSE stream; progression webhooks carry the details server-side.
Reusing one idempotency key for repeatable actions. Day 2 of a streak deduplicates into day 1.
Scope keys to the occurrence: user:action:date for dailies, user:action:attempt_n for attempts.
Batching 100 events for one participant and expecting them all to land.
The per-participant limit (default 100/min) applies per event. Spread large per-user replays over time and handle rate_limited items by re-queueing.
Calling points/badges endpoints directly from event flows, double-crediting what the rules engine already granted.
One event per user action; let rules fan out. Reserve direct value-movement endpoints (which need the gamify:write scope) for server-driven grants like support adjustments.
Ignoring per-item statuses in batch responses.
Check results[i].status. error means fix and re-send; rate_limited means re-queue with backoff; forbidden means your JWT is emitting for the wrong participant.
Developer FAQ
Common questions when integrating gamification with Bricqs.
Ready to ship?
Wire it up with the Bricqs SDK or API
Headless SDK for React UIs, REST API for any backend. Same engine behind both.
