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

# Lead Events

> Stream per-phone events into Setter so future sends can include or exclude leads based on what they did

For the endpoint spec (URL, auth, request/response shapes, examples), see [Send Lead Event](/campaigns/send-lead-event).

This page covers when to send events, how to configure a campaign send to consume them, and the patterns the time-window filter is designed for.

## Why

Some sends should depend on what a lead has *done*, not just which audience they're in:

* *"Don't send the May 17 'introducing Mastery' message to anyone who already purchased it."*
* *"For the +2hr 'you can still join' reminder, skip phones currently in the live event."*

You record those signals as **lead events** — small per-phone records like `mastery_purchase` or `webinar_attended`, each with a timestamp. At send-fire time, the campaign send filters its audience based on those events.

## Real-time vs bulk

The endpoint accepts either pattern — both produce the same rows, the filter logic doesn't care.

* **Real-time stream** — POST one event at a time as it happens (a purchase completes, a phone joins the live event). Best when you want the freshest possible data without a polling loop.
* **Bulk batch** — POST up to 50,000 events in one call. Best when your source system aggregates and you'd rather push periodically than per-event.

If you POST the same `(phoneE164, eventType, occurredAt)` more than once, the duplicate is silently dropped. Retries are safe.

## Configuring a send to consume events

When adding a send to your campaign sequence, enable **Event filter** and pick:

* **Mode** — `exclude` (skip leads that have the event) or `include` (only send to leads that have the event).
* **Event type** — must match the `eventType` strings you POST (case-sensitive).
* **Within last (minutes, optional)** — only consider events whose `occurredAt` is within this many minutes of the send firing.

The filter is applied at send-materialize time, \~60 seconds before the scheduled fire time.

## Sticky vs windowed events

Whether to use the time window depends on the event's meaning:

| Pattern                              | Filter config                                                                  | Use when                                                                |
| ------------------------------------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| **Sticky** (no window)               | `{ mode: "exclude", eventType: "mastery_purchase" }`                           | The event is permanent — once it happens, the lead is "done" forever.   |
| **Windowed** (with `within.minutes`) | `{ mode: "exclude", eventType: "webinar_attended", within: { minutes: 120 } }` | The event is temporary — "currently attending" only matters for \~2hrs. |

Without a window, "ever happened" matches. With a window, only events whose `occurredAt` falls inside the last N minutes from send-fire time match.

## Common patterns

**Exclude prior purchasers across a multi-day campaign:**

```json theme={null}
{ "mode": "exclude", "eventType": "mastery_purchase" }
```

No window — once a lead purchases, they stop receiving the upsell sequence on every subsequent day.

**Skip people in the live event for the "you can still join" reminder:**

```json theme={null}
{ "mode": "exclude", "eventType": "attended", "within": { "minutes": 120 } }
```

Window matches today's session only. Yesterday's attendees who haven't joined today still receive the reminder.

**Only message engaged leads (opt-in confirmation campaign):**

```json theme={null}
{ "mode": "include", "eventType": "double_opt_in_confirmed" }
```

Sends only to leads who completed the confirmation step.

## Combining with audience filtering

A send can have **both** an event filter (this page) **and** an [audience filter](/campaigns/audience-filtering) configured. Both must keep a recipient for them to receive the message — the filters are ANDed.

## Phone normalization

`phoneE164` values you POST go through libphonenumber. Both `+15551234567` and `15551234567` parse to the same canonical E.164. Numbers that don't parse are reported per-row in the response's `rejects[]` rather than failing the whole batch.
