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

# Filter Send Audience

> Narrow a scheduled campaign send to a real-time include or exclude list of leads

## Overview

Most campaign sends go to the full uploaded audience. This endpoint lets you narrow a specific send based on signals that aren't known at upload time — e.g. *"for tomorrow's send, only message leads who completed step X by tonight."*

POST an `include` or `exclude` list to a specific send up until 5 minutes before that send fires (the filter deadline).

<Note>
  **Enabling on a send**: in the campaign editor, toggle **"Apply filter from inbound webhook"** on for the target send. A send with the filter off ignores any webhook events targeting it.
</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.

## When the filter is applied

The send's recipient list is materialized about 60 seconds before `scheduledFor`. At that moment:

1. Every lead in the campaign's audience whose ingest status is `ok` is loaded.
2. Opted-out phone numbers are dropped.
3. The **most recent** filter event for that send is applied: `include` keeps only listed leads; `exclude` removes them.
4. One row per remaining lead is inserted into the per-recipient send queue.

If you POST multiple distinct filter events before the deadline, only the latest one matters. Idempotent replays of the exact same payload return `{ status: "already_received" }` and are no-ops.

## Lead matching

A lead in your filter payload matches an audience lead if **either** of these is true:

* The payload's `externalId` matches the audience lead's `external_id` column from the CSV (case-sensitive).
* The payload's `phoneE164` matches the normalized E.164 stored on the audience lead.

Leads in your filter payload that don't match any audience lead are silently ignored.

## Combining with lead events

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

## Example Usage

### HMAC signature

```bash theme={null}
SECRET="your-signing-secret"
BODY='{"campaignId":42,"sendId":137,"mode":"include","leads":[{"externalId":"abc123"}]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)

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

### Bearer API key

```bash theme={null}
curl -X POST https://chat.trysetter.com/api/v1/webhooks/campaigns/audience-filter \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "campaignId": 42,
    "sendId": 137,
    "mode": "exclude",
    "leads": [
      { "phoneE164": "+15551234567" },
      { "phoneE164": "+15557654321" }
    ]
  }'
```


## OpenAPI

````yaml POST /api/v1/webhooks/campaigns/audience-filter
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/campaigns/audience-filter:
    post:
      tags:
        - Campaigns
      summary: Filter Send Audience
      description: >-
        Narrows a scheduled campaign send's audience to a real-time list of
        leads. POST an `include` or `exclude` list to a specific send up until 5
        minutes before that send fires (the filter deadline).


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


        **Idempotency:** identical payload bodies are deduped per send via the
        SHA-256 of the body. A duplicate POST returns `{ status:
        'already_received' }`.


        **Most-recent-wins:** if you POST multiple distinct filter events for
        the same `sendId` before the deadline, only the latest is applied at
        materialize time. Replace the entire list when updating; deltas are not
        supported.
      operationId: submitAudienceFilter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AudienceFilterRequest'
            examples:
              include_by_external_id:
                summary: Include only specific leads (by externalId)
                value:
                  campaignId: 42
                  sendId: 137
                  mode: include
                  leads:
                    - externalId: your_lead_id_001
                    - externalId: your_lead_id_002
              exclude_by_phone:
                summary: Exclude specific leads (by phoneE164)
                value:
                  campaignId: 42
                  sendId: 137
                  mode: exclude
                  leads:
                    - phoneE164: '+15551234567'
                    - phoneE164: '+15557654321'
      responses:
        '200':
          description: Filter event recorded (or duplicate of an earlier payload)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceFilterResponse'
              examples:
                accepted:
                  summary: First delivery
                  value:
                    status: accepted
                    leadCount: 12345
                already_received:
                  summary: Identical payload replayed (no-op)
                  value:
                    status: already_received
        '400':
          description: >-
            Malformed body, missing required field, or unknown campaignId/sendId
            for this org
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Unknown campaignId / sendId
        '401':
          description: Auth failed — bad signature or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Invalid signature
        '403':
          description: Campaigns feature not enabled for this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Forbidden
        '409':
          description: Filter deadline already passed, or send is no longer pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                deadline_passed:
                  summary: Filter deadline elapsed (5 min before send)
                  value:
                    error: Filter deadline passed
                    deadlineUtc: '2026-05-15T13:55:00Z'
                send_not_pending:
                  summary: Send already started materializing
                  value:
                    error: Send no longer pending
                    status: queued
        '413':
          description: Payload exceeds 10 MB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                message: Payload too large
      security:
        - bearerAuth: []
        - signatureAuth: []
components:
  schemas:
    AudienceFilterRequest:
      type: object
      required:
        - campaignId
        - sendId
        - mode
        - leads
      properties:
        campaignId:
          type: integer
          description: Campaign that owns the targeted send.
        sendId:
          type: integer
          description: >-
            Send to filter. Must currently be in `pending` status with
            `audienceFilterMode = webhook_filtered`.
        mode:
          type: string
          enum:
            - include
            - exclude
          description: >-
            `include` — send only to listed leads. `exclude` — send to everyone
            EXCEPT listed leads.
        leads:
          type: array
          minItems: 1
          maxItems: 100000
          items:
            $ref: '#/components/schemas/AudienceFilterLead'
    AudienceFilterResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - accepted
            - already_received
          description: >-
            `accepted` on first delivery; `already_received` if this exact
            payload was already recorded for this send.
        leadCount:
          type: integer
          description: >-
            Count of leads in the accepted payload (omitted on
            `already_received`).
    ErrorResponse:
      type: object
      properties:
        message:
          type: string
          description: Error message
    AudienceFilterLead:
      type: object
      description: >-
        Lead identifier. Provide externalId, phoneE164, or both — externalId
        takes precedence when both match.
      properties:
        externalId:
          type: string
          description: Matches `external_id` from the audience CSV (case-sensitive).
        phoneE164:
          type: string
          description: >-
            E.164 phone number (with leading `+`). Matches the normalized phone
            on the audience lead.
  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`.

````