---
title: "Ingest events in batch"
method: POST
path: "/v1/events/batch"
tags: ["Events"]
---

# Ingest events in batch

`POST /v1/events/batch`

## Request

- Base URL: `https://api.scrip.dev`
- URL: `https://api.scrip.dev/v1/events/batch`
- Auth: HTTP bearer

Submit up to 100 events in one request, each accepted or rejected on its own with an outcome in `results` and its own `idempotency_key`.

## Headers

- `Idempotency-Key` string

## Request body

- BatchEventRequest: Up to 100 events to ingest in one request.
  - `events` EventRequest[], required: Events accepted or rejected independently, in request order; submit between 1 and 100 items
    - `event_data` object, required: JSON object exposed to rule conditions and action expressions as event data
    - `event_timestamp` string, date-time, required: Time the event occurred; rules evaluate against this timestamp rather than ingestion time
    - `external_id` string: Your system's identifier for the user who triggered this event. Mutually exclusive with participant_id - exactly one must be provided. Auto-creates a participant if this ID doesn't exist (based on the program's on_unknown_participant setting). Existing participants are automatically enrolled in the target program.
    - `idempotency_key` string, required: Your key for this event within the program. Send it here or in the Idempotency-Key header. Sending the same key again returns the same event.
    - `participant_id` string, uuid: Scrip's UUID for the participant. Mutually exclusive with external_id - exactly one must be provided. The participant is automatically enrolled in the target program if not already a member.
    - `program_id` string, uuid, required: Program that supplies the rules evaluated for this event
    - `recipient_external_id` string: Optional external identifier for a second participant the event is for, such as a gift recipient. Rules read it as recipient.id and recipient.external_id. It does not move rewards by itself: actions without a target still apply to the sender, so pay the recipient with a target such as {"participant_id": "${{ recipient.id }}"}. Mutually exclusive with recipient_id. Auto-creates the recipient if this ID doesn't exist (based on the program's on_unknown_participant setting), mirroring external_id. An existing recipient is automatically enrolled in the target program if not already a member.
    - `recipient_id` string, uuid: Optional Scrip UUID for a second participant the event is for, such as a gift recipient. Rules read it as recipient.id and recipient.external_id. It does not move rewards by itself: actions without a target still apply to the sender, so pay the recipient with a target such as {"participant_id": "${{ recipient.id }}"}. Mutually exclusive with recipient_external_id. Must reference an existing participant; they are automatically enrolled in the target program if not already a member.

## Response `202`

Batch accepted for async processing

- BatchEventResponse: The outcome of a batch of events: counts, and one result for each event in the order you sent them.
  - `error_count` integer: Number of events rejected before ingestion
  - `results` BatchEventResult[]: Per-item outcomes in the same order as the submitted events
    - `error` ErrResponse: The error body every failed request returns. Branch on `code`, and quote `request_id` when you contact support.
      - `code` string: The specific reason for the error. Branch on this value, not on message. The errors page lists every code
      - `details` object: Extra context for this code, documented per code on the errors page. A 500 never has it
      - `doc_url` string: A link to this code on the errors page
      - `fields` ErrorFieldDetail[]: Every failing field on a validation_error, reported in one pass
        - `field` string: The JSON name of the failing field, with dots for nested fields
        - `message` string: What to change in this field
        - `reason` string: Why the field failed: required, invalid_format, out_of_range, unknown_field, not_allowed or too_long
      - `message` string: What happened and what to do next, in one or two sentences
      - `param` string: The one request field at fault, when there is one
      - `request_id` string: The id of this request, the same req_ value as the X-Request-ID response header. Quote it when you contact support
      - `type` 'invalid_request' | 'authentication' | 'permission' | 'not_found' | 'conflict' | 'unprocessable' | 'rate_limited' | 'internal': The error family. It is one value per HTTP status: invalid_request, authentication, permission, not_found, conflict, unprocessable, rate_limited or internal
    - `event` IngestionResponse: An event that Scrip accepted, with its id and where to read its progress.
      - `event_timestamp` string, date-time: When the event occurred (from the ingestion request)
      - `external_id` string: Your system's identifier for the user, if provided
      - `id` string, uuid: Unique identifier for the created event
      - `idempotency_key` string: Client-provided unique key for deduplication
      - `participant_id` string, uuid: Participant UUID. May be null if only external_id was provided (resolution may be deferred to async processing).
      - `program_id` string, uuid: Program the event was ingested into
      - `status` 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED': The event's processing status right after it is accepted.
      - `status_url` string: Path to fetch this event and its processing status, such as `/v1/events/{id}`; the same path is in the `Location` header of the single-event response
    - `index` integer: Zero-based position of the corresponding item in the request array
    - `status` 'ACCEPTED' | 'ERROR': Whether Scrip queued this event (ACCEPTED) or rejected it (ERROR).
  - `success_count` integer: Number of events accepted for asynchronous processing
  - `total` integer: Number of submitted items, including accepted and rejected events

## Other responses

- `400`: Validation failed (code: validation_error | invalid_json)
- `401`: The API key is missing or not valid. See [Authentication](https://docs.scrip.dev/authentication).
- `403`: The caller is not allowed to do this.
- `429`: Too many requests. Wait the number of seconds in the `Retry-After` header, then retry. See [Limits and durations](https://docs.scrip.dev/limits#rate-limits).
- `500`: Something failed on Scrip's side. Retry the request, and quote `request_id` if it keeps failing. [Errors](https://docs.scrip.dev/errors) lists every code, and the [API introduction](https://docs.scrip.dev/api-reference/introduction) describes the error format.

---

[API](https://skmtc.dev/scrip/apis/scrip-api.md) · [All operations](https://skmtc.dev/scrip/apis/scrip-api/llms.txt) · [OpenAPI document](https://skmtc.dev/scrip/apis/scrip-api/revisions/c52d35da8d97?raw)
