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

# Campaigns

> How to set up and manage outbound campaigns using the Syllable Console interface.

A campaign is a planned and organized effort to reach out to a list of contacts over a period of time, with specified operating hours and rate of outreach (e.g., 1 call per hour).

To learn more, check out our [<u>Campaign Tutorial Video</u>](https://www.youtube.com/watch?v=764c-r8qNn8)  and [<u>blog post</u>](https://syllable.ai/blog/Build-an-AI-Test-Drive-Agent/) .

## Creating a campaign

To create a campaign, navigate to "Campaigns" in the sidenav, and click "Create campaign" in the top right.

<img src="https://mintcdn.com/actiumhealth/oCt9wAfIHMGvippm/images/campaigns_create.png?fit=max&auto=format&n=oCt9wAfIHMGvippm&q=85&s=67dd26ce8c5b5798e633b30c73cecebb" alt="campaigns_create.png" title="campaigns_create.png" className="mx-auto" style={{ width:"83%" }} width="1500" height="1640" data-path="images/campaigns_create.png" />

* **Name**: Name of your campaign
* **Description** (optional): Description of your campaign.
* **Labels** (optional): Add labels or tags.
* **Mode:** Select mode from Voice, SMS, Email.
* **Channel source**: Select a channel source, which is assigned to an outbound agent, that will be doing the outreach
* **Display ID**: Enter the phone number or caller ID that your recipients will see
* **Rate per hour**: Set the rate of outreach per hour (e.g., If set to 60, it will try to do 1 outreach per minute)
* **Campaign hours**: Set the days and hours of operation for your campaign. You can set it so that it will only run during weekdays, or normal business hours of 9 to 5.

## Batches

A batch is a grouping of phone numbers to call. Phone numbers to call are populated in a batch by uploading a CSV or manually adding a phone number. A campaign can have multiple batches, but a batch can only have one file upload.

### Creating a batch

To create a batch, click on a campaign, click the "Batches" tab and "Create batch" when you are viewing a campaign.

<img src="https://mintcdn.com/actiumhealth/oCt9wAfIHMGvippm/images/campaigns_batch_create.png?fit=max&auto=format&n=oCt9wAfIHMGvippm&q=85&s=08894db60ec4f5a704be8dc52e804e2c" alt="campaigns_batch_create.png" title="campaigns_batch_create.png" className="mx-auto" style={{ width:"83%" }} width="1554" height="706" data-path="images/campaigns_batch_create.png" />

* **Expires on** (optional): Can set an expiration date for the campaign to end. This may or may not occur after all the contacts are reached out to.
  * Note: Phone numbers in the batch will not be called after the expiry time
* **Auto-run batch:** This toggle is to immediately start running the campaign on the batch.
  * If enabled, the campaign will automatically start reaching out to the contact list when you click "Save". And new phone numbers added to the batch will be automatically called.
  * If disabled, the campaign will be paused when you click "Save", and you will need to click "Start" to have it begin reaching out.

### Uploading a batch

You can create a batch in a few ways:

* Directly through the API endpoint
* Upload a CSV file in the UI

To upload a file in the UI, click "Save and Upload". Batch CSVs must contain the following columns: (Download [<u>template</u>](https://docs.google.com/spreadsheets/d/1Jly1QCeT8Rv_hFOoPUekIzRHxT7B80oMKZLmqeeFrME/edit?usp=sharing))

* **reference\_id**: A unique identifier
* **target**: Phone number in format +14155551234
* Any other columns can be referenced in the agent prompt or message. For example, if the CSV has a column "first-name", the agent message can be configured to say something like "Hello vars.first-name" when that phone number is called. These additional columns are also accessible by the prompt. Reference [Variables](/resources/Variables) for more details.

<img src="https://mintcdn.com/actiumhealth/oCt9wAfIHMGvippm/images/campaigns_batch_template.png?fit=max&auto=format&n=oCt9wAfIHMGvippm&q=85&s=de8d60f2911a93de4b904d8e04e6dc02" alt="campaigns_batch_template.png" title="campaigns_batch_template.png" className="mx-auto" style={{ width:"88%" }} width="1048" height="218" data-path="images/campaigns_batch_template.png" />

<img src="https://mintcdn.com/actiumhealth/oCt9wAfIHMGvippm/images/campaigns_batches.png?fit=max&auto=format&n=oCt9wAfIHMGvippm&q=85&s=6d7b47dcbf0a3a328e6eeefbfaa2b546" alt="campaigns_batches.png" width="2882" height="1976" data-path="images/campaigns_batches.png" />

Batch statuses ([<u>See API reference</u>](https://docs.syllable.ai/api-reference/outboundbatches/create-outbound-communication-batch#response-status)):

* **Active**: When batch upload is successful and campaign is running (e.g., Auto-run is enabled)
* **Paused**: When batch upload is successful and campaign is paused (e.g., Auto-run is disabled)
* **Pending**: When batch is created, but there is no file uploaded yet or is being processed
* **Failed**: When batch upload is unsuccessful
* **Canceled**: When batch is canceled
* **Expired**: When batch is expired

**Tip**: While a campaign can go for weeks, it is best to have a batch run for a day.

## Testing your campaign

To test your campaign configuration and batch file upload, you can try sending one test call to see if it follows the specified business hours and rate of outreach.

* Go to a campaign, click on Batches tab
* Click on a batch and add a test number to the "Add to queue" section
* By clicking "Add", it will be added to the batch, and will follow the campaign settings for business hours and rate of outreach.
  * **Note**: Depending on your campaign settings, you may need to wait until the next scheduled outreach.

<img src="https://mintcdn.com/actiumhealth/oCt9wAfIHMGvippm/images/campaigns_batch_status.png?fit=max&auto=format&n=oCt9wAfIHMGvippm&q=85&s=6fb2a85476897ba34d9585b654c2b170" alt="campaigns_batch_status.png" width="2184" height="1756" data-path="images/campaigns_batch_status.png" />

## Campaign statuses

When running outbound campaigns, each request (voice call, SMS message, or email) moves through multiple statuses.

### Voice

| **Status** | **Meaning**                                                                                                                                                                                                                                                                                                                                                                  |
| :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DUPLICATE  | Communication was filtered out by validation as a duplicate                                                                                                                                                                                                                                                                                                                  |
| INVALID    | Communication was filtered out by validation for having an invalid destination                                                                                                                                                                                                                                                                                               |
| PENDING    | Communication has passed validation and is waiting to be sent                                                                                                                                                                                                                                                                                                                |
| CANCELED   | Communication had been PENDING but batch was canceled                                                                                                                                                                                                                                                                                                                        |
| COMPLETED  | Call was successfully placed without voicemail detection enabled                                                                                                                                                                                                                                                                                                             |
| BUSY       | Twilio: "Twilio dialed the number, but received a busy response."                                                                                                                                                                                                                                                                                                            |
| FAILED     | Twilio: "Twilio's carriers could not connect the call. Possible causes include the destination is unreachable, or the number may have been input incorrectly."                                                                                                                                                                                                               |
| NO-ANSWER  | Twilio: "Twilio dialed the number but no one answered before the timeout parameter value elapsed. This can be configured for each call, but by default is set to 60 seconds on [<u>outbound API calls</u>](https://www.twilio.com/docs/voice/api/call#parameters), and 30 seconds on [<u>outbound \<Dial> calls</u>](https://www.twilio.com/docs/voice/twiml/dial#timeout)." |
| MACHINE    | Call was successfully placed with voicemail detection enabled - voicemail detection determined that the call reached a voicemail                                                                                                                                                                                                                                             |
| HUMAN      | Call was successfully placed with voicemail detection enabled - voicemail detection determined that the call reached a human                                                                                                                                                                                                                                                 |
| UNKNOWN    | Call was successfully placed with voicemail detection enabled - voicemail detection was not able to determine whether the call reached a voicemail or a human                                                                                                                                                                                                                |

```mermaid placement="bottom-right" theme={null}
flowchart TD
  START([Outbound voice call])
  START --> V{Validation}

  V -->|Duplicate request in same batch| DUPLICATE[DUPLICATE<br/>Filtered as duplicate]
  V -->|Invalid destination| INVALID[INVALID<br/>Invalid destination]

  V -->|No validation issues| PENDING[PENDING<br/>Queued to send]

  PENDING -->|Batch canceled| CANCELED[CANCELED<br/>Was PENDING, batch canceled]

  PENDING -->|Twilio places call| TW{Call outcome}

  TW -->|Busy signal| BUSY[BUSY]
  TW -->|Carriers could not connect| FAILED[FAILED]
  TW -->|Timeout, no answer| NOANS[NO-ANSWER]

  TW -->|Connected| VMD{Voicemail detection enabled?}

  VMD -->|No| COMPLETED[COMPLETED<br/>Call placed, no VMD]
  VMD -->|Yes| VMH{Voicemail detection result}

  VMH -->|Voicemail| MACHINE[MACHINE]
  VMH -->|Human| HUMAN[HUMAN]
  VMH -->|Could not classify| UNKNOWN[UNKNOWN]

  style DUPLICATE fill:#f9f,stroke:#333
  style INVALID fill:#f9f,stroke:#333
  style CANCELED fill:#ffe,stroke:#333
```

### SMS

| **Status**          | **Meaning**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DUPLICATE           | Communication was filtered out by validation as a duplicate                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| INVALID             | Communication was filtered out by validation for having an invalid destination                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| PRIOR\_UNSUBSCRIBED | Communication was filtered out by validation because a previous communication to the same user has status UNSUBSCRIBED                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| PENDING             | Communication has passed validation and is waiting to be sent                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| CANCELED            | Communication had been PENDING but batch was canceled                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| SENT                | Twilio: "Twilio has received a confirmation from our Super Network partner advising they have accepted the message. Twilio has not received updated delivery information for your message. Typically, a sent status will be replaced by a delivered or undelivered status within seconds or minutes. Please note that US/Canada long code MMS often remain in sent status due to a lack of final delivery status updates from carriers. Once a message is older than 72 hours, receiving a further status is unlikely, although we will update the record if we receive one. Your messages will stay in sent status forever if you are sending messages using the deprecated [<u>/SMS/Message REST API Resource</u>](https://help.twilio.com/articles/223181028-Switching-from-SMS-Messages-resource-URI-to-Messages-resource-URI), which does not support delivery status updates." |
| ACCEPTED            | Twilio: "(Messaging Services only) Twilio has received your request to create the message from a Messaging Service. Twilio is determining the optimal 'From' number from your service."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| QUEUED              | Twilio: "Twilio has received your request to create the message. All new messages sent from a specific Twilio phone number are created with a status of queued."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| SENDING             | Twilio: "Twilio is in the process of sending the message. This status is usually only present for a very short time, and in most instances will not be visible in the console or logs."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| UNDELIVERED         | Twilio: "Twilio has received a delivery receipt indicating that the message was not delivered. This can happen for a number of reasons including carrier content filtering, availability of the destination handset, etc."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| DELIVERY\_FAILED    | Twilio "failed" status: "The message could not be sent. This can happen for various reasons including queue overflows, account suspensions and media errors (in the case of MMS)."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| DELIVERY\_UNKNOWN   | Twilio: "(Displayed in Messaging Insights) This indicates that a message has remained in the sent status for longer than 1 hour without receiving a further status update. This status is only shown in Messaging Insights; SMS records will remain in the sent status in the Message Log or /Messages API."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| DELIVERED           | Twilio SMS: "Twilio has received confirmation of message delivery from the carrier, (and, where available, the destination handset)."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| UNSUBSCRIBED        | During the conversation, the user opted out                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |

```mermaid theme={null}
flowchart TD
  START([Outbound SMS message])
  START --> V{Validation}

  V -->|Duplicate request in same batch| DUPLICATE[DUPLICATE]
  V -->|Invalid destination| INVALID[INVALID]
  V -->|Recipient previously opted out| PRIOR_UNSUB[PRIOR_UNSUBSCRIBED]

  V -->|No validation issues| PENDING[PENDING<br/>Queued to send]

  PENDING -->|Batch canceled| CANCELED[CANCELED]

  PENDING -->|Submitted to Twilio| PIPE{Twilio pipeline}

  PIPE -->|Send failure| DELFAIL[DELIVERY_FAILED]
  PIPE -->|Messaging Service| ACCEPTED[ACCEPTED]
  PIPE -->|Direct number| QUEUED[QUEUED]

  ACCEPTED --> QUEUED
  QUEUED --> SENDING[SENDING]
  SENDING --> SENT[SENT<br/>Accepted by carrier network]

  SENT -->|Carrier receipt| DELIVERED[DELIVERED]
  SENT -->|Not delivered| UNDELIV[UNDELIVERED]
  SENT -->|No update 1h+| DELUNK[DELIVERY_UNKNOWN<br/>Twilio Messaging Insights only]

  SENT --> CONV{During conversation?}
  CONV -->|User opts out| UNSUB[UNSUBSCRIBED]

  style DUPLICATE fill:#f9f,stroke:#333
  style INVALID fill:#f9f,stroke:#333
  style PRIOR_UNSUB fill:#f9f,stroke:#333
  style CANCELED fill:#ffe,stroke:#333
```

### Email

| **Status**          | **Meaning**                                                                                                                                                                                                                                                                                                                                  |
| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DUPLICATE           | Communication was filtered out by validation as a duplicate                                                                                                                                                                                                                                                                                  |
| INVALID             | Communication was filtered out by validation for having an invalid destination                                                                                                                                                                                                                                                               |
| PRIOR\_UNSUBSCRIBED | Communication was filtered out by validation because a previous communication to the same user has status UNSUBSCRIBED                                                                                                                                                                                                                       |
| PRIOR\_SPAM\_REPORT | Communication was filtered out by validation because a previous email to the same user has status SPAM\_REPORT                                                                                                                                                                                                                               |
| PRIOR\_DROPPED      | Communication was filtered out by validation because a previous email to the same user has status DROPPED                                                                                                                                                                                                                                    |
| PRIOR\_BOUNCED      | Communication was filtered out by validation because a previous email to the same user has status BOUNCED                                                                                                                                                                                                                                    |
| PENDING             | Communication has passed validation and is waiting to be sent                                                                                                                                                                                                                                                                                |
| CANCELED            | Communication had been PENDING but batch was canceled                                                                                                                                                                                                                                                                                        |
| PROCESSED           | Email has been accepted by SendGrid and will be sent                                                                                                                                                                                                                                                                                         |
| DROPPED             | SendGrid: "There are a number of reasons your email will not even be sent to a recipient for delivery. This event informs your system when an email has been dropped. Further, it provides a reason for the drop, such as if we've found spam content (if spam checker app is enabled) or we see the recipient has unsubscribed previously." |
| DEFERRED            | SendGrid: "When an email cannot immediately be delivered, but it hasn't been completely rejected, the deferred event fires. Sometimes called a soft bounce, SendGrid will continue to try for 72 hours to deliver a deferred message. After 72 hours, the deferral turns into a block."                                                      |
| BOUNCED             | SendGrid: "If a server cannot or will not deliver a message, SendGrid fires a bounce event. Bounces often are caused by outdated or incorrectly entered email addresses. Many times you won't know a bounced email address until it bounces, so this event can help you ensure it doesn't bounce again by removing it from your lists."      |
| DELIVERED           | SendGrid: Email has been accepted by the receiving server (we don't know for a fact that it's in the user's inbox at this point, but can get more info if it moves to OPENED, CLICKED, SPAM\_REPORT, or UNSUBSCRIBED)                                                                                                                        |
| OPENED              | SendGrid: "This event fires every time your email is viewed with images turned on"                                                                                                                                                                                                                                                           |
| CLICKED             | Recipient clicked a link in the email body                                                                                                                                                                                                                                                                                                   |
| SPAM\_REPORT        | During the conversation, the user reported the email they received as spam                                                                                                                                                                                                                                                                   |
| UNSUBSCRIBED        | During the conversation, the user unsubscribed                                                                                                                                                                                                                                                                                               |

```mermaid theme={null}
flowchart TD
  START([Outbound email])
  START --> V{Validation}

  V -->|Duplicate request in same batch| DUPLICATE[DUPLICATE]
  V -->|Invalid destination| INVALID[INVALID]
  V -->|Recipient previously unsubscribed| PRIOR_UNSUB[PRIOR_UNSUBSCRIBED]
  V -->|Recipient previously reported spam| PRIOR_SPAM[PRIOR_SPAM_REPORT]
  V -->|Previous email was DROPPED| PRIOR_DROP[PRIOR_DROPPED]
  V -->|Previous email BOUNCED| PRIOR_BOUNCE[PRIOR_BOUNCED<br/>Prior BOUNCED]

  V -->|No validation issues| PENDING[PENDING<br/>Queued to send]

  PENDING -->|Batch canceled| CANCELED[CANCELED]

  PENDING -->|Accepted by SendGrid| PROCESSED[PROCESSED<br/>Will be sent]

  PROCESSED --> SG{SendGrid lifecycle}

  SG -->|Never sent| DROPPED[DROPPED]
  SG -->|Soft bounce, retry up to 72h| DEFERRED[DEFERRED]
  SG -->|Hard failure| BOUNCED[BOUNCED]

  DEFERRED -->|Eventually| BOUNCED
  DEFERRED -->|Success| DELIVERED[DELIVERED<br/>Accepted by receiving server]

  PROCESSED -->|If not dropped/deferred/bounced first| DELIVERED

  DELIVERED --> ENG{Recipient engagement}

  ENG --> OPENED[OPENED]
  ENG -->|Clicks a link in email| CLICKED[CLICKED]
  ENG -->|Reports spam| SPAM[SPAM_REPORT]
  ENG -->|Unsubscribes| UNSUB[UNSUBSCRIBED]

  OPENED --> CLICKED
  OPENED --> SPAM
  OPENED --> UNSUB

  style DUPLICATE fill:#f9f,stroke:#333
  style INVALID fill:#f9f,stroke:#333
  style PRIOR_UNSUB fill:#f9f,stroke:#333
  style PRIOR_SPAM fill:#f9f,stroke:#333
  style PRIOR_DROP fill:#f9f,stroke:#333
  style PRIOR_BOUNCE fill:#f9f,stroke:#333
  style CANCELED fill:#ffe,stroke:#333
```

## Voicemail detection

You can enable voicemail detection for your campaigns. When a call connects, the system can determine whether the answer is from a live person or a voicemail system.

If a voicemail is detected, the agent can leave a pre-recorded message for the recipient. If no voicemail is detected, the agent or workflow proceeds with a live interaction, based on the agent's instructions.

**Configuration Options**

You can configure detection timeouts to control when and how voicemail is identified:

* **Pre-speech timeout:** Time allowed before the callee begins speaking. If the user does not speak within this timeout, the conversation proceeds normally.
* **Post-speech timeout:** Time allowed after the callee finishes speaking. If the user stays silent for this duration, the system considers the speech ended and makes a decision.
* **Overall timeout:** Maximum total duration for voicemail detection to run before returning a result.

These values can be adjusted per campaign or per workspace to balance speed and accuracy.

**Prompt**

When voicemail detection is enabled, the agent is by default instructed to leave a voicemail\_message explaining why the agent called and asking the user to call back. You can override this behavior by adding information to your prompt, for example:

> If you reach a voicemail set the voicemail\_message to: "Hello, this is New Bay Health calling to assist you with scheduling your recent referral. We're eager to assist you at your earliest convenience. Please give us a call back at 555-123-1234. Our scheduling team is available Monday through Friday from 7:00 AM to 5:00 PM to assist you. Thank you."

## Webhooks

Webhooks give your systems real-time visibility into call progress. When an outbound call session changes status  - for example, when a call connects, fails, or completes  - Syllable sends an HTTP request to your registered webhook URL with details about the event. This allows you to programmatically track call outcomes, trigger alerting on failures, and update internal records without polling or waiting for a campaign to finish.

<Info>
  Webhooks are configured at the **campaign level**. You can route different statuses to different endpoints  - for example, send `FAILED` events to an alerting service and `COMPLETED` events to a logging system.
</Info>

### Configuring webhooks

Webhooks can be configured through the Console campaign configuration screen, the [API](/api-reference/outboundcampaigns/create-outbound-communication-campaign), or the SDK.

Each webhook requires:

| Parameter          | Type              | Description                                                                    |
| :----------------- | :---------------- | :----------------------------------------------------------------------------- |
| `url`              | String            | The HTTPS URL to receive webhook payloads.                                     |
| `trigger_statuses` | Array of strings  | The call statuses that trigger this webhook (e.g., `["COMPLETED", "FAILED"]`). |
| `request_method`   | String            | HTTP method for the request. Must be `POST`, `PUT`, or `PATCH`.                |
| `auth_values`      | Object (optional) | Authentication configuration. See [Authentication](#webhook-authentication).   |

<Tabs>
  <Tab title="Console">
    1. Navigate to your outbound campaign in the Console.
    2. In the campaign configuration, find the **Webhooks** section.
    3. Click **Add webhook**.
    4. Enter your HTTPS URL (e.g., `https://your-server.com/hooks/syllable`).
    5. Select the trigger statuses you want to receive (e.g., `COMPLETED`, `FAILED`).
    6. Choose your authentication method and provide the required credentials.
    7. Save the campaign.
  </Tab>

  <Tab title="SDK (Python)">
    ```python theme={null}
    from syllable_sdk import SyllableSDK, models
    import os

    with SyllableSDK(
        api_key_header=os.getenv("SYLLABLESDK_API_KEY_HEADER", ""),
    ) as client:

        res = client.outbound.campaigns.create(request={
            "campaign_name": "Appointment Reminders",
            "campaign_variables": {"key": "value"},
            "caller_id": "19995551234",
            "active_days": [
                models.DaysOfWeek.MON,
                models.DaysOfWeek.TUE,
                models.DaysOfWeek.WED,
                models.DaysOfWeek.THU,
                models.DaysOfWeek.FRI,
            ],
            "webhooks": [
                {
                    "trigger_statuses": [
                        models.ChannelManagerStatus.COMPLETED,
                        models.ChannelManagerStatus.FAILED,
                    ],
                    "url": "https://your-server.com/hooks/syllable",
                    "request_method": "POST",
                    "auth_values": {
                        "hmac_secret": "your-base64-encoded-secret",
                    },
                },
            ],
        })
    ```
  </Tab>

  <Tab title="SDK (TypeScript)">
    ```typescript theme={null}
    import { SyllableSDK } from "syllable-sdk";

    const client = new SyllableSDK({
      apiKeyHeader: process.env["SYLLABLESDK_API_KEY_HEADER"] ?? "",
    });

    const result = await client.outbound.campaigns.create({
      campaignName: "Appointment Reminders",
      campaignVariables: { key: "value" },
      callerId: "19995551234",
      activeDays: ["mon", "tue", "wed", "thu", "fri"],
      webhooks: [
        {
          triggerStatuses: ["COMPLETED", "FAILED"],
          url: "https://your-server.com/hooks/syllable",
          requestMethod: "POST",
          authValues: {
            hmac_secret: "your-base64-encoded-secret",
          },
        },
      ],
    });
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST https://api.syllable.cloud/api/v1/outbound/campaigns \
      -H "Content-Type: application/json" \
      -H "Syllable-API-Key: $SYLLABLE_API_KEY" \
      -d '{
        "campaign_name": "Appointment Reminders",
        "campaign_variables": {"key": "value"},
        "caller_id": "19995551234",
        "active_days": ["mon", "tue", "wed", "thu", "fri"],
        "webhooks": [
          {
            "trigger_statuses": ["COMPLETED", "FAILED"],
            "url": "https://your-server.com/hooks/syllable",
            "request_method": "POST",
            "auth_values": {
              "hmac_secret": "your-base64-encoded-secret"
            }
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

### Webhook payload

When a call transitions to a status that matches one of your configured triggers, Syllable sends an `HTTP POST` request with `Content-Type: application/json`.

```json theme={null}
{
  "session_id": 842,
  "to_number": "+14155550100",
  "from_number": "+12125550199",
  "batch_upload_filename": "outreach_batch_jan2026.csv",
  "channel_manager_status": "FAILED",
  "status_changed_at": "2026-01-15T18:32:07Z",
  "error_message": "The destination number is not a valid phone number"
}
```

| Field                    | Type              | Description                                                                                                                                      |
| :----------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_id`             | Number            | Unique identifier for the outbound call session. Stable across retries  - use this for correlation and deduplication.                            |
| `to_number`              | String            | The destination phone number of the outbound call.                                                                                               |
| `from_number`            | String            | The originating phone number that placed the call.                                                                                               |
| `batch_upload_filename`  | String \| null    | The filename of the batch upload that included this call. Use this to tie the event back to your original source file. `null` if not applicable. |
| `channel_manager_status` | String            | The call's new status. See [Campaign statuses](#campaign-statuses) for the full list.                                                            |
| `status_changed_at`      | String (ISO 8601) | UTC timestamp of when the status change occurred. Reflects the actual event time, not the webhook delivery time.                                 |
| `error_message`          | String \| null    | Description of the error if the call failed. `null` when no error occurred.                                                                      |

<Tip>
  For most integrations, subscribing to `COMPLETED` and `FAILED` covers the primary use cases: logging successful calls and alerting on failures.
</Tip>

### Webhook authentication

Webhook requests are authenticated using an HMAC signature to ensure they originate from Syllable.

**HMAC signature**

When you configure HMAC authentication, you provide a Base64-encoded shared secret (RFC 4648, decoding to 32–512 bytes of key material). Each webhook request will include two headers:

| Header                 | Description                                                                         |
| :--------------------- | :---------------------------------------------------------------------------------- |
| `X-Syllable-Timestamp` | Unix timestamp of when the webhook was sent.                                        |
| `X-Syllable-Signature` | HMAC-SHA256 signature of `{timestamp}.{raw_body}`, computed with the shared secret. |

To verify a request:

1. Extract the `X-Syllable-Timestamp` and `X-Syllable-Signature` headers.
2. Concatenate the timestamp, a literal `.`, and the raw request body: `{timestamp}.{raw_body}`.
3. Compute the HMAC-SHA256 of that string using your shared secret.
4. Compare your computed signature to `X-Syllable-Signature` using a **constant-time comparison** to prevent timing attacks.
5. Reject requests where the timestamp is older than 5 minutes to mitigate replay attacks.

<CodeGroup>
  ```python Python (Flask) theme={null}
  import hmac
  import hashlib
  import base64
  import time
  from flask import Flask, request, abort

  app = Flask(__name__)

  WEBHOOK_SECRET_B64 = "your-base64-encoded-secret"

  @app.route("/hooks/syllable", methods=["POST"])
  def handle_webhook():
      timestamp = request.headers.get("X-Syllable-Timestamp", "")
      signature = request.headers.get("X-Syllable-Signature", "")

      if abs(time.time() - int(timestamp)) > 300:
          abort(401, "Stale timestamp")

      secret = base64.b64decode(WEBHOOK_SECRET_B64)
      message = f"{timestamp}.".encode() + request.get_data()
      expected = hmac.new(secret, message, hashlib.sha256).hexdigest()

      if not hmac.compare_digest(expected, signature):
          abort(401, "Invalid signature")

      event = request.get_json()
      print(f"Session {event['session_id']}: {event['channel_manager_status']}")

      return "OK", 200

  if __name__ == "__main__":
      app.run(port=8000)
  ```

  ```typescript TypeScript (Express) theme={null}
  import express from "express";
  import crypto from "crypto";

  const app = express();
  const WEBHOOK_SECRET_B64 = "your-base64-encoded-secret";

  app.post(
    "/hooks/syllable",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const timestamp = req.headers["x-syllable-timestamp"] as string;
      const signature = req.headers["x-syllable-signature"] as string;

      if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
        return res.status(401).send("Stale timestamp");
      }

      const secret = Buffer.from(WEBHOOK_SECRET_B64, "base64");
      const message = Buffer.concat([
        Buffer.from(`${timestamp}.`),
        req.body,
      ]);
      const expected = crypto
        .createHmac("sha256", secret)
        .update(message)
        .digest("hex");

      if (
        !crypto.timingSafeEqual(
          Buffer.from(expected),
          Buffer.from(signature)
        )
      ) {
        return res.status(401).send("Invalid signature");
      }

      const event = JSON.parse(req.body.toString());
      console.log(
        `Session ${event.session_id}: ${event.channel_manager_status}`
      );

      res.sendStatus(200);
    }
  );

  app.listen(8000, () => console.log("Listening on :8000"));
  ```
</CodeGroup>

### Delivery and retries

Your webhook endpoint must respond with a `2xx` HTTP status code **within 10 seconds** to acknowledge receipt. The timeout covers the full HTTP response cycle. We recommend accepting the payload and processing it asynchronously if your handling logic may take longer.

If your endpoint returns a non-`2xx` status code or does not respond within the timeout, Syllable retries delivery:

* Up to **2 retry attempts** (i.e., 3 total attempts) over approximately **15 minutes**.
* If all attempts fail, the delivery is marked as failed.

**Webhook delivery order is not guaranteed.** Use the `status_changed_at` timestamp to determine the correct sequence of events. Because retries may cause duplicate deliveries, your endpoint should treat webhooks as **at-least-once delivery**. Use `session_id` + `channel_manager_status` + `status_changed_at `together as a deduplication key.

If you don't receive events, check:

* Your endpoint is publicly reachable over HTTPS.
* The trigger statuses on the webhook match the call outcomes you're testing.
* Your endpoint returns `200` within 10 seconds.
* Failed deliveries are visible in the Console under the campaign's webhook status.

## Monitoring your campaign

Keep track of your campaign's progress by viewing the batch status or overall campaign status. You can also see the outbound campaign calls, summaries, and transcripts in Sessions.

For more detailed steps, check out our [<u>Campaign Tutorial Video</u>](https://www.youtube.com/watch?v=764c-r8qNn8)  and [<u>blog post</u>](https://syllable.ai/blog/Build-an-AI-Test-Drive-Agent/) .
