Skip to main content
A participant represents a user in your system. You identify participants with your own external IDs, and Scrip handles enrollment, balances, and state tracking.

Creating Participants

Explicitly via the API

curl -X POST https://api.scrip.dev/v1/participants \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "user_123",
    "program_id": "{program_id}",
    "email": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Doe"
  }'
FieldRequiredDescription
external_idYesYour application’s user identifier
program_idNoEnroll the participant in a program on creation
statusNoACTIVE (default), SUSPENDED, or CLOSED
emailNoContact email (validated)
phoneNoContact phone number
first_nameNoFirst name
last_nameNoLast name
display_nameNoDisplay name
attributesNoKey-value metadata
tagsNoList of string labels
If a participant with the same external_id already exists, the call upserts: it updates the existing participant instead of creating a duplicate.

Automatically on first event

When a program’s on_unknown_participant is set to CREATE (the default), participants are created the first time you send an event with an unrecognized external_id:
curl -X POST https://api.scrip.dev/v1/events \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "program_id": "{program_id}",
    "external_id": "user_456",
    "idempotency_key": "signup-user-456",
    "event_timestamp": "2025-01-15T10:30:00Z",
    "event_data": {"type": "signup"}
  }'
Scrip creates the participant, enrolls them in the program, and processes the event in one step. Set on_unknown_participant to REJECT if you want to require explicit creation before sending events. See Programs for details.

Auto-enrollment of existing participants

When an event targets a participant who exists but isn’t enrolled in the event’s program, Scrip automatically enrolls them. This applies across all identity paths (external_id, participant_id, recipient_id, recipient_external_id). Enrollment status is per-program and separate from the participant’s own status; inactive enrollments (FROZEN, LOCKED, or CLOSED) are reactivated. The on_unknown_participant setting only controls whether new participants are created. It does not affect enrollment of existing ones.

Profile Fields

Participants support optional profile fields for contact and display information: email, phone, first_name, last_name, and display_name. These are returned in all participant responses (create, update, get, list). Profile fields follow a three-state convention:
Value sentBehavior
Omitted (or null)Existing value is preserved
Non-empty stringField is set to the new value
Empty string ("")Field is cleared
The email field is validated on both create and update. Invalid addresses return a 400 error.
# Update profile fields
curl -X PATCH https://api.scrip.dev/v1/participants/{id} \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "display_name": "Jane Doe"
  }'
# Clear a field by sending an empty string
curl -X PATCH https://api.scrip.dev/v1/participants/{id} \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": ""
  }'

Identifiers

Participants have two IDs:
IDFormatUse
idUUIDScrip’s internal identifier, used in all API paths
external_idStringYour application’s user ID, used for lookups
To find a participant by your external ID:
GET /v1/participants?external_id=user_123
Use the returned id for all subsequent API calls. The list endpoint returns identifiers, status, and profile fields. To get the full participant state in one call, use the detail endpoint:
GET /v1/participants/{id}
The detail response includes balances, tags, counters, attributes, tiers, program_ids, and profile fields inline.

Status

A participant’s status controls what operations are allowed:
StatusDescription
ACTIVENormal operation. All actions allowed.
SUSPENDEDTemporary freeze (e.g., fraud review, compliance hold). Reversible.
CLOSEDDeactivated. Can be transitioned back to ACTIVE or SUSPENDED.

What’s allowed by status

ActionACTIVESUSPENDED / CLOSED
CREDITYesBlocked
DEBITYesBlocked
HOLDYesBlocked
RELEASEYesBlocked
FORFEITYesBlocked
VOID_HOLDYesBlocked
COUNTERYesBlocked
TAG / UNTAGYesAllowed
SET_ATTRIBUTEYesAllowed
SET_TIERYesAllowed
BROADCASTYesAllowed
SCHEDULE_EVENTYesAllowed
Balance and counter actions are blocked because a suspended or closed participant should not accumulate value. Metadata actions (tags, attributes, tiers) are allowed because you still need to manage inactive accounts: tagging a participant with fraud_confirmed, setting attributes for audit trails, or adjusting tiers during a review period.

How this affects events

Events whose resolved actor or recipient is SUSPENDED or CLOSED are rejected at the ingestion boundary, before any rule runs:
  • When ingestion resolves identities synchronously, the API returns 422 with code participant_suspended or participant_closed.
  • In queue-based ingestion mode, the event is accepted (202) and then recorded as a terminal FAILED event carrying the same error_code, with a matching event.failed webhook.
Direct API management of tags, attributes, and tiers is not gated.
Idempotent replays are unaffected: retrying an event that was accepted while the participant was still ACTIVE returns the original event rather than a lifecycle rejection.
PATCH /v1/participants/{id}/status
{"status": "SUSPENDED"}

Balances

Check a participant’s current balances across all assets:
GET /v1/participants/{id}/balances
Balances are split by asset into three buckets:
BucketDescription
AVAILABLESpendable immediately
HELDReserved for authorization holds, pending settlements, or fraud review
DEFERREDNot yet matured. Credits with a future matures_at land here and move to AVAILABLE automatically when the date passes.
You can perform manual balance adjustments directly on a participant:
POST /v1/participants/{id}/balances/adjust
{
  "program_id": "{program_id}",
  "asset_id": "{asset_id}",
  "type": "CREDIT",
  "amount": "500",
  "description": "Customer service goodwill credit"
}
See Balance Operations for hold, release, forfeit, and other operations.

Transaction History

View the ledger entries for a participant:
GET /v1/participants/{id}/activity/history
Returns a chronological list of credits, debits, holds, and releases with journal entry details.

Time-Range Filters

The endpoint supports two independent time-range filters that can be combined (AND semantics):
FilterFilters onUse case
from / tocreated_at (system ingestion time)When the journal entry was recorded
event_from / event_toevent_timestamp (event occurrence time)When the originating event occurred in your system
Entries without an originating event fall back to created_at for event_from/event_to filtering. You can also retrieve the events processed for a participant:
GET /v1/participants/{id}/activity/events

Participant State

Each participant carries state that rules can read and update:
TypePurposeExample
TagsBoolean flagsvip, first_purchase
CountersNumeric trackerspurchase_count: 42
AttributesKey-value stringsregion: "US"
TiersStatus levels with benefitsloyalty_tier: "gold"
Tags are stored verbatim and matched case-sensitively. Pick one casing convention (these docs use lowercase) and use it in both TAG actions and conditions. Counters support high-precision numerics. See State Management for how to read, update, and use state in rules.