Skip to main content
POST
/
v1
/
webhooks
Outbound Webhooks
curl --request POST \
  --url https://api.sequenzy.com/v1/webhooks \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "name": "<string>",
  "url": "<string>",
  "events": [
    "<string>"
  ]
}
'
{
  "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"
      }
    ]
  }
}

Outbound Webhooks

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

Create

name
string
required
Human-readable webhook name.
url
string
required
Absolute HTTPS endpoint URL.
events
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.
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

# 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:
{
  "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.
{
  "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.
{
  "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"
    }
  }
}
{
  "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:
{
  "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

{
  "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"
      }
    ]
  }
}

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.