Sending an Event
| Field | Required | Description |
|---|---|---|
program_id | Yes | Which program’s rules should evaluate this event |
external_id | One of external_id or participant_id | Your application’s user ID |
participant_id | One of external_id or participant_id | Scrip’s internal participant UUID |
recipient_id | No | Route rewards to a different participant by Scrip UUID (e.g., gifting). Mutually exclusive with recipient_external_id. |
recipient_external_id | No | Route rewards to a different participant by your external ID. Mutually exclusive with recipient_id. |
idempotency_key | Yes | Unique key per program for exactly-once processing (1-255 characters) |
event_timestamp | Yes | When the event occurred in your system (RFC 3339) |
event_data | Yes | JSON object available to rules as event.* in CEL |
Timestamps
Every event carries two timestamps:| Timestamp | Set by | Purpose |
|---|---|---|
event_timestamp | You, at ingestion | When the event occurred in your system |
created_at | Scrip, on receipt | When Scrip received and stored the event |
-
event_timestampis the logical clock. It becomes thenowvariable in CEL expressions, so rule conditions that compare against time use the event’s occurrence time, not the current time. This keeps evaluation deterministic across retries and reprocessing. -
created_atis the ingestion clock. Thefromandtoquery parameters on list endpoints filter oncreated_at, notevent_timestamp. This makes incremental polling reliable: you can track “give me everything since my last sync” without missing late-arriving events. To filter by when events actually occurred, use theevent_fromandevent_toparameters instead. Both pairs can be used simultaneously (AND semantics).
event_timestamp is customer-supplied, it can differ from created_at. A batch import might backdate events to last month, or clock skew might push timestamps slightly into the future. Rules evaluate against the current rule definitions regardless of event_timestamp. A backdated event runs against today’s rule definitions, but active_from / active_to windows are checked against the event’s event_timestamp, so a backdated event inside a past window still fires time-windowed rules. See Time-Windowed Rules.
Processing Pipeline
Events are processed asynchronously. The API confirms receipt, not validity. Business validation (program existence and status, participant resolution) and rule evaluation happen in the background. Existing participants are automatically enrolled in the target program if not already members; see Programs: Enrollment for enrollment behavior and theon_unknown_participant setting. Validation errors surface via event.failed webhooks.
Read-after-write visibility: the ID returned by the
202 is durable, but in queue-based ingestion mode the event may briefly 404 on both GET /v1/events/{id} and GET /v1/events/by-key (typically well under a second) until the async consumer materializes it. Poll until the ID resolves. Every accepted submission eventually becomes readable: either as a processed event or as status FAILED with an error_code if it was rejected asynchronously.Event Lifecycle
| Status | Meaning |
|---|---|
PENDING | Received, waiting for processing |
PROCESSING | Worker is evaluating rules |
COMPLETED | All matching rules evaluated and their allowed actions executed |
FAILED | Validation failed (invalid or inactive program, suspended or closed participant) or a rule action was blocked |
RECEIVED | Legacy status on historical rows; new events enter at PENDING. Still accepted as a filter value. |
Events whose resolved actor or recipient is
SUSPENDED or CLOSED are rejected at the ingestion boundary, before any rule runs: 422 with code participant_suspended or participant_closed when identities resolve synchronously, or a terminal FAILED event with the same error_code in queue-based mode. See Participants: How this affects events.error_code (when the failure has a classified code, such as participant_suspended or program_inactive) on the event resource and the event.failed webhook payload, so you can branch on failure type without string-matching the error message.
If you have webhook endpoints configured, Scrip sends event.completed or event.failed notifications when processing finishes. This lets your application react to processing results without polling.
Transient failures (infrastructure errors, timeouts) retry automatically with exponential backoff (2s, 4s, 8s, 16s, 32s), up to 5 retries. Validation failures are terminal; fix the cause and retry manually:
PENDING for a fresh set of attempts.
Idempotency
Theidempotency_key ensures exactly-once processing per program. If you send the same program_id + idempotency_key combination more than once, the duplicate is ignored and the original event is returned. This applies regardless of whether the payload differs.
If a network timeout occurs, re-send the same request. The duplicate is safely deduplicated.
Treat idempotency keys as unique identifiers per intent. If the payload needs to change (e.g., correcting an amount), use a new key.
Use meaningful, deterministic idempotency keys like
order-12345-completed or referral-user456-signup. Avoid random UUIDs, which defeat the purpose of deduplication.Event Data Design
Theevent_data payload becomes the event variable in CEL expressions. Design it with rules in mind:
| Tip | Rationale |
|---|---|
Include a consistent type field | Clean rule filtering: event.type == "purchase" |
| Keep amounts as numbers | Avoids double() casting in rules |
| Use snake_case for field names | Consistency with the API |
| Include context for debugging | store_id, order_id help troubleshoot |
Rules reference
event_data fields directly as event.amount, event.category, etc. If a rule references a field that isn’t in the payload, the condition evaluates to false and the rule doesn’t match. Use has() for fields that only appear on some events. See CEL Expressions.Batch Ingestion
Send up to 100 events in a single request:202 response reports per-event outcomes: each entry in results is either accepted (with the full event object) or error (with an error_code and message). Valid events proceed even when siblings fail. A 400 is returned only when the envelope itself is malformed (zero events, more than 100, or unparseable JSON). Business validation errors surface later via event.failed webhooks.
Event Routing
By default, rule actions apply to the event’s participant. To credit a different participant, include their identifier inevent_data and reference it in the rule action’s target:
target field’s external_id accepts a CEL expression that resolves to a participant’s external ID. You can also use participant_id to resolve by Scrip UUID. The target participant must exist (they are automatically enrolled if not already a member of the program).
Rules always evaluate conditions against the event’s participant (user_123). Only the action’s credit is routed to the target. See Rule Actions for more on static and dynamic targeting.