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

# Outbound Webhooks

> Create and manage outbound webhook endpoints for email, subscriber, and sequence lifecycle events.

# Outbound Webhooks

You can configure an endpoint to receive Sequenzy email, subscriber, and sequence lifecycle events as signed JSON POST requests.

## Create

<ParamField body="name" type="string" required>
  Human-readable webhook name.
</ParamField>

<ParamField body="url" type="string" required>
  Absolute HTTPS endpoint URL.
</ParamField>

<ParamField body="events" type="string[]">
  Event types to receive. Omit this field to receive the default lifecycle
  events: sent, delivered, delayed, bounced, complained, email unsubscribed, and
  invalid subscriber, and subscriber unsubscribed. Add opened, clicked, replied,
  subscriber.updated, sequence.finished, and sequence.failed explicitly if you
  need engagement, inbound reply, profile sync, or sequence lifecycle events.
</ParamField>

```bash theme={null}
curl -X POST "https://api.sequenzy.com/v1/webhooks" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production webhook",
    "url": "https://example.com/sequenzy/webhooks",
    "events": ["email.delivered", "email.bounced", "email.opened"]
  }'
```

## Manage

```bash theme={null}
# List endpoints
curl "https://api.sequenzy.com/v1/webhooks" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"

# Update endpoint (disable it but keep it, with its delivery history)
curl -X PATCH "https://api.sequenzy.com/v1/webhooks/webhook_123" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "disabled"}'

# Permanently delete endpoint and its delivery history
curl -X DELETE "https://api.sequenzy.com/v1/webhooks/webhook_123" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"

# Add signing secret
curl -X POST "https://api.sequenzy.com/v1/webhooks/webhook_123/secrets" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"

# Remove signing secret
curl -X DELETE "https://api.sequenzy.com/v1/webhooks/webhook_123/secrets/sec_123" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"

# Send a test event
curl -X POST "https://api.sequenzy.com/v1/webhooks/webhook_123/test" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"

# List latest delivery attempt summary
curl "https://api.sequenzy.com/v1/webhooks/webhook_123/deliveries/del_123/attempts" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"

# Replay a delivery
curl -X POST "https://api.sequenzy.com/v1/webhooks/webhook_123/deliveries/del_123/replay" \
  -H "Authorization: Bearer $SEQUENZY_API_KEY"
```

Updating an endpoint URL, enabling a disabled endpoint, sending a test event, and replaying a delivery reset the endpoint failure state so a recovered receiver can be tested immediately.

## Event Payload

Each webhook delivery sends a signed JSON event to your endpoint. Email events include Sequenzy IDs and, when available, your subscriber external ID or the `subscriberExternalId` stored on a transactional send:

```json theme={null}
{
  "id": "evt_123",
  "type": "email.sent",
  "object": "event",
  "metric": "sent",
  "created_at": "2026-05-05T12:00:00.000Z",
  "data": {
    "email_send_id": "send_123",
    "message_id": "ses-message-id",
    "subscriber_id": "sub_123",
    "external_id": "customer_123",
    "recipient": "user@example.com",
    "subject": "Welcome",
    "email_type": "campaign",
    "computed_lists": [
      {
        "key": "recommendedEvents",
        "items": [
          {
            "id": "evt_123",
            "title": "Auckland Theatre"
          }
        ],
        "exposures": [
          {
            "slot": 1,
            "id": "evt_123",
            "title": "Auckland Theatre"
          }
        ]
      }
    ],
    "metadata": {
      "source": "campaign"
    }
  }
}
```

Webhook payloads use one canonical snake\_case field per value. `external_id` is included when the recipient is linked to a subscriber with a customer-owned external ID, or when a single-recipient transactional send included `subscriberExternalId` even if no subscriber exists. `email.sent` events include `computed_lists` when campaign personalization selected per-recipient list items for that email; `items` preserves the original campaign data objects.

`email.replied` fires when an inbound reply is stored. The payload includes reply, conversation, and original send context plus the reply body text, HTML body, and stripped text. Attachment bodies are not included; only metadata is delivered.

```json theme={null}
{
  "id": "evt_reply",
  "type": "email.replied",
  "object": "event",
  "metric": "replied",
  "created_at": "2026-05-05T12:00:00.000Z",
  "data": {
    "reply_id": "reply_123",
    "conversation_id": "conv_123",
    "conversation_message_id": "msg_123",
    "email_send_id": "send_123",
    "message_id": "reply-message-id",
    "in_reply_to": "ses-message-id",
    "campaign_id": "camp_123",
    "subscriber_id": "sub_123",
    "external_id": "customer_123",
    "from_email": "user@example.com",
    "from_name": "Ada",
    "recipient": "user@example.com",
    "subject": "Re: Welcome",
    "email_type": "campaign",
    "body_text": "Thanks for the update.",
    "body_html": "<p>Thanks for the update.</p>",
    "stripped_text": "Thanks for the update.",
    "has_attachments": false,
    "attachment_count": 0,
    "received_at": "2026-05-05T12:00:00.000Z"
  }
}
```

Subscriber events include the current subscriber profile when one exists. `subscriber.invalid` fires when a subscriber add attempt cannot create a sendable subscriber because the attempted email is missing, syntactically invalid, has an invalid/blocked domain, or is already suppressed from previous delivery failures. `subscriber.updated` fires when `email`, `external_id`, `first_name`, `last_name`, `custom_attributes`, or a non-unsubscribe `status` change occurs. Active to unsubscribed changes emit `subscriber.unsubscribed`; they only also emit `subscriber.updated` when another profile field changes in the same update. List and tag changes do not emit `subscriber.updated`.

```json theme={null}
{
  "id": "evt_invalid",
  "type": "subscriber.invalid",
  "object": "event",
  "metric": "invalid",
  "created_at": "2026-05-05T12:00:00.000Z",
  "data": {
    "email": "bad@gmai.com",
    "external_id": "customer_123",
    "first_name": "Ada",
    "custom_attributes": {
      "plan": "pro"
    },
    "reason_code": "invalid_email_domain",
    "reason": "Invalid domain for \"bad@gmai.com\": Domain is blacklisted (typosquatting or disposable email)",
    "metadata": {
      "source": "add_subscriber"
    }
  }
}
```

```json theme={null}
{
  "id": "evt_789",
  "type": "subscriber.updated",
  "object": "event",
  "metric": "updated",
  "created_at": "2026-05-05T12:00:00.000Z",
  "data": {
    "subscriber_id": "sub_123",
    "external_id": "customer_new",
    "email": "new@example.com",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "status": "active",
    "custom_attributes": {
      "plan": "pro"
    },
    "changed_fields": ["email", "external_id", "custom_attributes"],
    "previous": {
      "email": "old@example.com",
      "external_id": "customer_old",
      "custom_attributes": {
        "plan": "starter"
      }
    }
  }
}
```

Sequence lifecycle events include the subscriber email, external ID when available, and the event data recorded for that sequence enrollment:

```json theme={null}
{
  "id": "evt_456",
  "type": "sequence.finished",
  "object": "event",
  "metric": "finished",
  "created_at": "2026-05-05T12:00:00.000Z",
  "data": {
    "sequence_id": "seq_welcome",
    "sequence_name": "Welcome sequence",
    "automation_id": "seq_welcome",
    "automation_name": "Welcome sequence",
    "automation_token_id": "token_123",
    "token_id": "token_123",
    "subscriber_id": "sub_123",
    "external_id": "customer_123",
    "email": "user@example.com",
    "lifecycle": "finished",
    "status": "completed",
    "current_node_id": "node_end",
    "event": {
      "id": "event_123",
      "name": "sequence_finished",
      "properties": {
        "sequence_id": "seq_welcome",
        "sequence_name": "Welcome sequence",
        "automation_id": "seq_welcome",
        "automation_name": "Welcome sequence",
        "automation_token_id": "token_123",
        "token_id": "token_123",
        "lifecycle": "finished",
        "status": "completed",
        "current_node_id": "node_end"
      }
    }
  }
}
```

## Responses

<ResponseExample>
  ```json 201 theme={null}
  {
    "success": true,
    "webhook": {
      "id": "webhook_123",
      "name": "Production webhook",
      "url": "https://example.com/sequenzy/webhooks",
      "status": "enabled",
      "events": ["email.delivered", "email.bounced", "email.opened"],
      "consecutiveFailures": 0,
      "circuitOpenedAt": null,
      "circuitOpenUntil": null,
      "signingSecret": "whsec_...",
      "signingSecrets": [
        {
          "id": "sec_123",
          "prefix": "whsec_abcd12",
          "createdAt": "2026-05-05T12:00:00.000Z"
        }
      ]
    }
  }
  ```

  ```json 400 theme={null}
  {
    "success": false,
    "error": "Unsupported webhook event type: email.unknown"
  }
  ```

  ```json 401 theme={null}
  {
    "success": false,
    "error": "Missing API key. Provide via x-api-key header or Authorization: Bearer <key>"
  }
  ```

  ```json 404 theme={null}
  {
    "success": false,
    "error": "Webhook not found"
  }
  ```
</ResponseExample>

## Verify Requests

Webhook requests include `X-Sequenzy-Timestamp` and `X-Sequenzy-Signature`. Build the signed payload as `v1:{timestamp}:{raw_request_body}`, compute an HMAC-SHA256 digest with each active webhook signing secret, and compare it to any `v1=` signature value.

If a webhook has multiple active signing secrets, Sequenzy still sends one POST request and includes one `v1=` signature per secret in the same header. Add a new secret, deploy it in your receiver, then remove the old secret.
