> ## 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.

# Send Lead Event

> Record a per-phone event so future campaign sends can include or exclude that lead

## Overview

Generic per-phone event stream. Use this to tell Setter "this phone number did event X at time T" so future campaign sends can include or exclude that lead based on whether the event happened (optionally within a time window).

Typical event types: `mastery_purchase`, `webinar_attended`, `trial_started`, `cart_abandoned`. You choose the names — they're free strings scoped to your organization.

<Note>
  **Shape-agnostic**: this endpoint accepts either real-time per-event POSTs (one event per call) or bulk batches (up to 50,000 events per call). Both produce the same rows; the filter logic doesn't care which shape was used.
</Note>

## Authentication

Pick the auth mode for inbound webhooks once per organization, in **Settings → Webhooks**:

* **API key** (default) — Bearer-token auth using any active org-level API key.
* **HMAC** — generate a signing key, sign the raw request body with HMAC-SHA256, send as `X-Setter-Signature: sha256=<hex>`. Multiple signing keys can be active concurrently for rotation.

## How sends consume these events

When configuring a send under Campaigns → Sequence → Add a send, 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` you POST.
* **Within last (minutes, optional)** — only consider events whose `occurredAt` is within this many minutes of the send firing.

Examples:

* Skip anyone who's ever purchased Mastery: `{ mode: "exclude", eventType: "mastery_purchase" }` (no window — sticky event)
* Skip anyone currently in the live event: `{ mode: "exclude", eventType: "webinar_attended", within: { minutes: 120 } }` (windowed — "currently attending")

The filter is applied at send-materialize time alongside any audience-filter event. Both filters can be active on the same send and are ANDed.

## Idempotency and validation

The unique key `(organizationId, phoneE164, eventType, occurredAt)` makes retries safe — re-POSTing the same batch returns `duplicates = received` and `inserted = 0`.

Invalid rows (bad phone, missing/invalid `occurredAt`, oversize metadata) are rejected per-row, not per-batch — you get a structured `rejects[]` array back rather than a 400 for the whole call. The first 50 rejection reasons are returned for debugging.

## Phone normalization

`phoneE164` is parsed via libphonenumber. Accepts the value with or without leading `+` and tolerates common formatting variants. Numbers that don't parse are reported in `rejects[]` with reason `invalid phoneE164`.

## Caps

| Limit                          | Value     |
| ------------------------------ | --------- |
| Events per request             | 50,000    |
| Request body size              | 10 MB     |
| `eventType` length             | 128 chars |
| `metadata` per event (encoded) | 8 KB      |

## Example Usage

### Real-time single event (Bearer API key)

```bash theme={null}
curl -X POST https://chat.trysetter.com/api/v1/webhooks/lead-events \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": 42,
    "eventType": "mastery_purchase",
    "events": [
      {
        "phoneE164": "+15551234567",
        "occurredAt": "2026-05-12T18:34:00Z",
        "metadata": { "orderId": "ord_9F2", "amountUsd": 297 }
      }
    ]
  }'
```

### Bulk batch (HMAC signature)

```bash theme={null}
SECRET="your-signing-secret"
BODY='{"organizationId":42,"eventType":"webinar_attended","events":[{"phoneE164":"+15551234567","occurredAt":"2026-05-15T13:00:00Z"},{"phoneE164":"+447700900123","occurredAt":"2026-05-15T13:05:11Z"}]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)

curl -X POST https://chat.trysetter.com/api/v1/webhooks/lead-events \
  -H "Content-Type: application/json" \
  -H "X-Setter-Signature: sha256=$SIG" \
  --data-raw "$BODY"
```


## OpenAPI

````yaml POST /api/v1/webhooks/lead-events
openapi: 3.0.3
info:
  title: Setter AI API
  description: API for Setter AI appointment booking assistant
  version: 1.0.0
  contact:
    email: support@trysetter.com
servers:
  - url: https://chat.trysetter.com
    description: Production server
security:
  - bearerAuth: []
paths:
  /api/v1/webhooks/lead-events:
    post:
      tags:
        - Campaigns
      summary: Send Lead Event
      description: >-
        Generic per-phone event stream. Use this to record that a phone number
        did event X at time T, so future campaign sends can include or exclude
        that lead based on whether the event happened (optionally within a time
        window).


        **Typical event types:** `mastery_purchase`, `webinar_attended`,
        `trial_started`, `cart_abandoned`. You choose the names — they're free
        strings scoped to your organization.


        **Auth:** per-organization webhook auth — either a Bearer API key OR an
        HMAC-SHA256 signature over the raw body. Configure under Settings →
        Webhooks.


        **Idempotency:** the unique key `(organizationId, phoneE164, eventType,
        occurredAt)` makes retries safe. Re-POSTing the same batch returns
        `duplicates = received` and `inserted = 0`.


        **Validation:** invalid rows (bad phone, missing/invalid `occurredAt`,
        oversize metadata) are rejected per-row, not per-batch — you get a
        `rejects[]` array back rather than a 400 for the whole call.
      operationId: submitLeadEvents
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeadEventsRequest'
            examples:
              single_event:
                summary: Single event (real-time stream pattern)
                value:
                  organizationId: 42
                  eventType: mastery_purchase
                  events:
                    - phoneE164: '+15551234567'
                      occurredAt: '2026-05-12T18:34:00Z'
                      metadata:
                        orderId: ord_9F2
                        amountUsd: 297
              bulk_batch:
                summary: Bulk batch (pre-aggregated snapshot)
                value:
                  organizationId: 42
                  eventType: webinar_attended
                  events:
                    - phoneE164: '+15551234567'
                      occurredAt: '2026-05-15T13:00:00Z'
                    - phoneE164: '+447700900123'
                      occurredAt: '2026-05-15T13:05:11Z'
                    - phoneE164: '+4915757956355'
                      occurredAt: '2026-05-15T13:08:30Z'
      responses:
        '200':
          description: >-
            Batch processed. Inspect `inserted` / `duplicates` / `rejected` for
            outcome detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeadEventsResponse'
              examples:
                all_new:
                  summary: All entries new
                  value:
                    status: accepted
                    received: 3
                    inserted: 3
                    duplicates: 0
                    rejected: 0
                    rejects: []
                mixed:
                  summary: Some duplicates + some validation rejects
                  value:
                    status: accepted
                    received: 5
                    inserted: 3
                    duplicates: 1
                    rejected: 1
                    rejects:
                      - index: 4
                        reason: invalid phoneE164
        '400':
          description: >-
            Malformed body, missing required field, or `events` array empty /
            over the 50000 cap
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missing_field:
                  summary: Required field missing
                  value:
                    message: eventType must be a non-empty string
                too_many_events:
                  summary: Batch too large
                  value:
                    message: events cannot exceed 50000 entries
        '401':
          description: Auth failed — invalid API key or bad HMAC signature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Invalid or missing API key
        '403':
          description: Campaigns feature not enabled for this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Forbidden
        '413':
          description: Payload exceeds 10 MB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Payload too large
      security:
        - bearerAuth: []
        - signatureAuth: []
components:
  schemas:
    LeadEventsRequest:
      type: object
      required:
        - organizationId
        - eventType
        - events
      properties:
        organizationId:
          type: integer
          description: Your Setter organization ID. Auth credentials must match this org.
        eventType:
          type: string
          maxLength: 128
          description: >-
            Free string scoped to your org. We recommend snake_case (e.g.
            `mastery_purchase`).
        events:
          type: array
          minItems: 1
          maxItems: 50000
          items:
            $ref: '#/components/schemas/LeadEvent'
    LeadEventsResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - accepted
        received:
          type: integer
          description: Number of entries in the input `events` array.
        inserted:
          type: integer
          description: Newly stored rows.
        duplicates:
          type: integer
          description: >-
            Entries that collided with an existing `(organizationId, phoneE164,
            eventType, occurredAt)` row.
        rejected:
          type: integer
          description: >-
            Entries that failed validation (bad phone, invalid date, oversize
            metadata).
        rejects:
          type: array
          description: First 50 rejected entries with a reason (debugging aid).
          items:
            type: object
            properties:
              index:
                type: integer
              reason:
                type: string
    ErrorResponse:
      type: object
      properties:
        message:
          type: string
          description: Error message
    LeadEvent:
      type: object
      required:
        - phoneE164
        - occurredAt
      properties:
        phoneE164:
          type: string
          description: >-
            Recipient phone (E.164, with or without leading `+` — normalized via
            libphonenumber).
        occurredAt:
          type: string
          format: date-time
          description: >-
            ISO 8601 timestamp of when the event happened. Used by sends
            configured with a `within.minutes` window.
        metadata:
          type: object
          description: >-
            Optional metadata for your own records (≤ 8 KB encoded). Not
            interpreted by the filter logic.
          additionalProperties: true
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Bearer token authentication using your API key
    signatureAuth:
      type: apiKey
      in: header
      name: X-Setter-Signature
      description: >-
        HMAC-SHA256 signature of the raw request body, formatted as
        `sha256=<hex>`. Sign with any active org-level webhook signing key
        (manage them under Settings → Webhooks). Used by org webhook auth mode
        `hmac`.

````