> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scrip.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Participants

> Create and manage the users in your programs

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

```bash theme={null}
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"
  }'
```

| Field          | Required | Description                                     |
| -------------- | -------- | ----------------------------------------------- |
| `external_id`  | Yes      | Your application's user identifier              |
| `program_id`   | No       | Enroll the participant in a program on creation |
| `status`       | No       | `ACTIVE` (default), `SUSPENDED`, or `CLOSED`    |
| `email`        | No       | Contact email (validated)                       |
| `phone`        | No       | Contact phone number                            |
| `first_name`   | No       | First name                                      |
| `last_name`    | No       | Last name                                       |
| `display_name` | No       | Display name                                    |
| `attributes`   | No       | Key-value metadata                              |
| `tags`         | No       | List 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`:

```bash theme={null}
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](/guides/programs#enrollment) 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 sent          | Behavior                      |
| ------------------- | ----------------------------- |
| Omitted (or `null`) | Existing value is preserved   |
| Non-empty string    | Field 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.

```bash theme={null}
# 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"
  }'
```

```bash theme={null}
# 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:

| ID            | Format | Use                                                |
| ------------- | ------ | -------------------------------------------------- |
| `id`          | UUID   | Scrip's internal identifier, used in all API paths |
| `external_id` | String | Your application's user ID, used for lookups       |

To find a participant by your external ID:

```bash theme={null}
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:

```bash theme={null}
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:

| Status      | Description                                                         |
| ----------- | ------------------------------------------------------------------- |
| `ACTIVE`    | Normal operation. All actions allowed.                              |
| `SUSPENDED` | Temporary freeze (e.g., fraud review, compliance hold). Reversible. |
| `CLOSED`    | Deactivated. Can be transitioned back to `ACTIVE` or `SUSPENDED`.   |

### What's allowed by status

| Action           | `ACTIVE` | `SUSPENDED` / `CLOSED` |
| ---------------- | -------- | ---------------------- |
| `CREDIT`         | Yes      | Blocked                |
| `DEBIT`          | Yes      | Blocked                |
| `HOLD`           | Yes      | Blocked                |
| `RELEASE`        | Yes      | Blocked                |
| `FORFEIT`        | Yes      | Blocked                |
| `VOID_HOLD`      | Yes      | Blocked                |
| `COUNTER`        | Yes      | Blocked                |
| `TAG` / `UNTAG`  | Yes      | **Allowed**            |
| `SET_ATTRIBUTE`  | Yes      | **Allowed**            |
| `SET_TIER`       | Yes      | **Allowed**            |
| `BROADCAST`      | Yes      | **Allowed**            |
| `SCHEDULE_EVENT` | Yes      | **Allowed**            |

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.

<Note>
  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.
</Note>

```bash theme={null}
PATCH /v1/participants/{id}/status
{"status": "SUSPENDED"}
```

## Balances

Check a participant's current balances across all assets:

```bash theme={null}
GET /v1/participants/{id}/balances
```

Balances are split by asset into three buckets:

| Bucket      | Description                                                                                                               |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `AVAILABLE` | Spendable immediately                                                                                                     |
| `HELD`      | Reserved for authorization holds, pending settlements, or fraud review                                                    |
| `DEFERRED`  | Not 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:

```bash theme={null}
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](/guides/balance-operations) for hold, release, forfeit, and other operations.

## Transaction History

View the ledger entries for a participant:

```bash theme={null}
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):

| Filter                    | Filters on                                | Use case                                           |
| ------------------------- | ----------------------------------------- | -------------------------------------------------- |
| `from` / `to`             | `created_at` (system ingestion time)      | When the journal entry was recorded                |
| `event_from` / `event_to` | `event_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:

```bash theme={null}
GET /v1/participants/{id}/activity/events
```

## Participant State

Each participant carries state that rules can read and update:

| Type       | Purpose                     | Example                 |
| ---------- | --------------------------- | ----------------------- |
| Tags       | Boolean flags               | `vip`, `first_purchase` |
| Counters   | Numeric trackers            | `purchase_count: 42`    |
| Attributes | Key-value strings           | `region: "US"`          |
| Tiers      | Status levels with benefits | `loyalty_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](/guides/state-management) for how to read, update, and use state in rules.
