Webhook Setup
Webhooks send HTTP POST requests to your server when events occur in SendSeven -- a message is received, a conversation is closed, a contact is created, and more. This enables real-time integrations without polling the API.
Required Scopes
| Scope | Purpose |
|---|---|
webhooks:read | List and view webhooks |
webhooks:create | Create new webhook endpoints |
webhooks:update | Update webhook configuration |
webhooks:delete | Delete webhooks |
How Webhooks Work
- You register a webhook endpoint URL and select events to subscribe to
- SendSeven immediately sends a one-time verification request to your URL; your endpoint must echo the challenge back to prove ownership (see Verify your endpoint)
- Once verified, when a subscribed event occurs SendSeven sends an HTTP POST to your URL
- Your server processes the event and returns a 2xx status code to acknowledge receipt
- If delivery fails, SendSeven retries with exponential backoff
Endpoint Requirements
Your webhook endpoint must:
- Use HTTPS (HTTP endpoints are not accepted)
- Return a 2xx status code within 10 seconds (the connection itself must open within 5 seconds)
- Accept POST requests with a JSON body
- Be publicly accessible from the internet. If a firewall restricts inbound traffic, allow SendSeven's outbound IP addresses
- Answer directly, without a redirect. SendSeven does not follow redirects: a
3xxresponse counts as a failed delivery (also for the verification request), so register the final URL - Resolve only to public IP addresses. The address is checked again on every delivery, not just when you save the webhook. Private, loopback, link-local, carrier-grade NAT (
100.64.0.0/10) and other non-public addresses (IPv4 and IPv6) are refused, and the delivery fails withWebhook URL is not allowed
The delivery history stores the first 1,000 characters of your endpoint's response body.
IP Allowlisting
All outbound requests from SendSeven come from these three fixed IPv4 addresses. That covers webhook deliveries, verification requests, test events and manual retries, as well as other outbound HTTP calls SendSeven makes on your behalf (for example API Call nodes in flows):
| IPv4 address |
|---|
136.92.3.138 |
34.107.59.177 |
35.246.202.227 |
- Allowlist all three. Any single delivery may come from any of these addresses, and consecutive deliveries to the same endpoint can come from different ones. Allowing only one or two will cause intermittent failures.
- IPv4 only. SendSeven does not send outbound traffic over IPv6, so no IPv6 rule is needed (your endpoint's hostname still needs an IPv4
Arecord). - Changes are announced in advance. If this list ever changes, we announce it ahead of time and keep the old addresses working during a transition period, so you can update your rules without downtime.
Troubleshooting: if deliveries or the verification request fail with a connection timeout (the connection never opens within 5 seconds) even though your endpoint works when you call it yourself, a firewall, WAF, hosting provider or bot protection layer is most likely dropping SendSeven's requests. Check that all three addresses above are allowed through every layer in front of your server, then trigger verification again (see Verify your endpoint).
Create a Webhook
POST /api/v1/webhook-endpoints
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | User-friendly name for the webhook (1-255 chars) |
url | string | Yes | HTTPS endpoint URL |
subscribed_events | array | Yes | Array of event types to subscribe to |
authorization_header | string | No | Authorization header value (e.g., Bearer token123) |
retry_strategy | string | No | exponential (default), linear, or none |
max_retries | integer | No | Max retry attempts (default: 8, max: 15) |
timeout_seconds | integer | No | Per-attempt timeout in seconds (default: 10, accepted range 5-60). Every attempt is capped at 10 seconds, so values above 10 have no effect. |
source_filter_mode | string | No | all (default) receives events from every channel; selected restricts them to the channels/integrations below |
filtered_channel_ids | array | No | Channel IDs to receive events from (only used when source_filter_mode is selected) |
filtered_email_integration_ids | array | No | Email integration IDs to receive events from (only used when source_filter_mode is selected) |
curl
curl -X POST "https://api.sendseven.com/api/v1/webhook-endpoints" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "CRM Integration",
"url": "https://yourapp.com/webhooks/sendseven",
"subscribed_events": [
"message.received",
"message.sent",
"conversation.created",
"conversation.closed",
"contact.created"
]
}'
Response (201 Created)
Creation returns the signing secret, not the full endpoint object. Note the field name: it is webhook_id, not id.
{
"webhook_id": "wh_abc123",
"secret_key": "whsec_9f2c4b8e1d6a7053ef4c1b9a8d2e6f30",
"message": "Store this secret key securely. It will not be shown again."
}
secret_key nowThis is the only time the secret is ever returned. You need it to verify webhook signatures, and it cannot be retrieved later -- only replaced via POST /api/v1/webhook-endpoints/{webhook_id}/regenerate-secret, which invalidates the old one.
Use the returned webhook_id with GET /api/v1/webhook-endpoints/{webhook_id} if you need the full endpoint object (its own id field carries the same value).
Verify Your Endpoint
Immediately after creation (and again whenever you change the endpoint's url, or call the verify endpoint below), SendSeven sends a one-time verification request to your URL. This is a challenge-response handshake that proves you control the endpoint. The webhook stays inactive and receives no events until it is verified (is_verified: false on the endpoint object).
The verification request does not carry an X-Sendseven-Signature header. It is the only exception to signature verification — every real event delivery is signed. A handler written strictly against the Signature Verification guide will reject this request with a 401 unless you special-case it. See the note in that guide.
The verification request
SendSeven sends:
-
Method:
POSTto your registered webhook URL -
Headers:
Header Example Notes Content-Typeapplication/jsonX-Sendseven-EventverificationIdentifies this as the verification handshake, not a real event X-Sendseven-Timestamp1770000000Unix timestamp (seconds) AuthorizationBearer …Only if you set authorization_headerwhen creating the webhookThere is no
X-Sendseven-Signatureheader on this request. -
Body:
{
"type": "sendseven_verification",
"challenge": "a1b2c3d4e5f6...",
"webhook_id": "wh_abc123",
"timestamp": "2026-02-10T18:00:00Z"
}
The response SendSeven expects
Your endpoint must reply with:
-
Status:
200 OK -
Body: JSON echoing the challenge back unchanged:
{ "challenge": "a1b2c3d4e5f6..." }
Returning the challenge as plain text, under a different key, or a bare 200 with no body will not verify the endpoint. You must respond within the endpoint's timeout_seconds (default and maximum 10s), and the challenge itself is valid for 5 minutes from when it was issued.
If your endpoint responds correctly, it becomes is_verified: true and is_active: true and starts receiving events. If it does not, the webhook is created but stays unverified — fix your handler and re-trigger verification:
POST /api/v1/webhook-endpoints/{webhook_id}/verify
curl -X POST "https://api.sendseven.com/api/v1/webhook-endpoints/wh_abc123/verify" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
Example handler
A minimal endpoint that handles both the unsigned verification request and signed events:
from fastapi import FastAPI, Request, Response, HTTPException
app = FastAPI()
@app.post("/webhooks/sendseven")
async def handle_webhook(request: Request):
body = await request.body() # raw bytes — capture before parsing
# The verification handshake is unsigned. Detect it and echo the challenge.
if request.headers.get("X-Sendseven-Event") == "verification":
import json
payload = json.loads(body)
return {"challenge": payload["challenge"]}
# Every real event IS signed — verify before trusting it.
# (see the Signature Verification guide for verify_signature)
signature = request.headers.get("X-Sendseven-Signature", "")
timestamp = request.headers.get("X-Sendseven-Timestamp", "")
if not verify_signature(body, signature, timestamp):
raise HTTPException(status_code=401, detail="Invalid signature")
# ... handle the event ...
return Response(status_code=200)
List Webhooks
GET /api/v1/webhook-endpoints
curl -X GET "https://api.sendseven.com/api/v1/webhook-endpoints" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"
This endpoint returns a bare JSON array of endpoint objects -- there is no items / pagination envelope, because a tenant's webhook list is small and unpaginated:
[
{
"id": "wh_abc123",
"tenant_id": "tenant_xyz",
"name": "CRM Integration",
"url": "https://yourapp.com/webhooks/sendseven",
"has_authorization_header": false,
"subscribed_events": [
"message.received",
"message.sent",
"conversation.created",
"conversation.closed",
"contact.created"
],
"source_filter_mode": "all",
"filtered_channel_ids": null,
"filtered_email_integration_ids": null,
"is_active": true,
"is_verified": false,
"retry_strategy": "exponential",
"max_retries": 8,
"timeout_seconds": 10,
"last_success_at": "2026-02-10T18:04:11Z",
"last_failure_at": null,
"last_error": null,
"consecutive_failures": 0,
"created_at": "2026-02-10T18:00:00Z",
"updated_at": null
}
]
Update a Webhook
Update a webhook's URL, events, or active status. Only include the fields you want to change.
PATCH /api/v1/webhook-endpoints/{webhook_id}
| Field | Type | Description |
|---|---|---|
name | string | Updated name |
url | string | Updated endpoint URL |
subscribed_events | array | Updated event subscriptions (replaces existing list) |
is_active | boolean | Enable or disable the webhook |
authorization_header | string | Updated auth header (empty string to remove) |
curl -X PATCH "https://api.sendseven.com/api/v1/webhook-endpoints/wh_crm_sync" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"subscribed_events": [
"contact.created",
"contact.updated",
"conversation.created",
"conversation.closed",
"message.received"
]
}'
Updating subscribed_events replaces the entire event list. Include all events you want to subscribe to, not just the new ones.
Delete a Webhook
DELETE /api/v1/webhook-endpoints/{webhook_id}
curl -X DELETE "https://api.sendseven.com/api/v1/webhook-endpoints/wh_analytics" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
View Delivery History
Check delivery status and response times for a webhook:
GET /api/v1/webhook-endpoints/{webhook_id}/deliveries
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
page_size | integer | 50 | Items per page (1--100) |
status | string | -- | pending, success, failed, retrying, dead_letter, or queued (stored while the endpoint is suspended -- see Circuit breaker) |
event_type | string | -- | e.g. message.sent, conversation.created |
curl -X GET "https://api.sendseven.com/api/v1/webhook-endpoints/wh_crm_sync/deliveries?page=1&page_size=10" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"
This endpoint's pagination fields are flat (total, page, page_size, has_more) rather than nested under a pagination object:
{
"items": [
{
"id": "del_001",
"tenant_id": "tenant_xyz",
"webhook_endpoint_id": "wh_crm_sync",
"event_type": "contact.created",
"event_id": "evt_5f1c9d",
"attempt_number": 1,
"status": "success",
"response_status_code": 200,
"response_time_ms": 145,
"error_message": null,
"next_retry_at": null,
"created_at": "2026-02-10T17:30:00Z",
"completed_at": "2026-02-10T17:30:00Z",
"can_retry": false
},
{
"id": "del_002",
"tenant_id": "tenant_xyz",
"webhook_endpoint_id": "wh_crm_sync",
"event_type": "message.received",
"event_id": "evt_7a3b2e",
"attempt_number": 3,
"status": "failed",
"response_status_code": 500,
"response_time_ms": 2300,
"error_message": "Internal Server Error",
"next_retry_at": null,
"created_at": "2026-02-10T17:35:00Z",
"completed_at": "2026-02-10T17:35:02Z",
"can_retry": true
}
],
"total": 156,
"page": 1,
"page_size": 10,
"has_more": true
}
Deliveries with can_retry: true (status failed or dead_letter) can be replayed manually -- see the retry endpoint below.
Retry Policy
If your endpoint returns a non-2xx status code or times out, SendSeven retries. With the default exponential strategy, the delay before each retry grows and then caps at 30 minutes:
| Retry | Delay after the failed attempt |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 10 seconds |
| 3rd retry | 30 seconds |
| 4th retry | 2 minutes |
| 5th retry | 5 minutes |
| 6th retry | 10 minutes |
| 7th retry and beyond | 30 minutes (capped) |
Retries continue until max_retries attempts have been made (default 8, configurable 0–15). With linear strategy, retries are spaced a fixed 60 seconds apart; with none, no retries are attempted. After the retry budget is exhausted, the delivery is marked as permanently failed.
Manually retry a delivery
A delivery whose can_retry is true (status failed or dead_letter) can be replayed on demand. This creates a new delivery attempt; the original row is left in place for audit.
POST /api/v1/webhook-endpoints/{webhook_id}/deliveries/{delivery_id}/retry
Required Scope: webhooks:update
curl -X POST "https://api.sendseven.com/api/v1/webhook-endpoints/wh_crm_sync/deliveries/del_002/retry" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
{
"success": true,
"delivery_id": "del_009",
"previous_delivery_id": "del_002",
"message": "Delivery queued for retry"
}
Circuit breaker: suspension, not silent loss
If a webhook accumulates 20 consecutive failures, SendSeven suspends it for a bounded 12-hour window rather than disabling it outright. During suspension:
- Matching events are not dropped — they are stored as
queueddeliveries (up to 10,000 per endpoint) and replayed in original order once the endpoint recovers. - SendSeven re-verifies your endpoint automatically on an escalating schedule: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h after suspension.
- You receive email notifications across the lifecycle (suspended, reactivated, permanently disabled).
The endpoint's state is visible on the endpoint object: suspended_at is set while suspended, next_reactivation_at holds the next automatic check, and reactivation_attempts counts checks made so far.
| Outcome | What happens |
|---|---|
| A re-verification succeeds (automatic or triggered by you) | is_active returns to true, suspended_at clears, and the queued backlog is replayed in created_at order |
| Verification succeeds but the first replayed event cannot open a connection (timed out, refused or reset) | The endpoint is suspended again without using up a step of the schedule and is checked again later; the 12-hour window still applies |
| The final check at +12h still fails | The endpoint is permanently deactivated and the queued deliveries are moved to dead_letter |
To recover immediately without waiting for the next scheduled check, fix your endpoint and trigger verification yourself:
curl -X POST "https://api.sendseven.com/api/v1/webhook-endpoints/wh_abc123/verify" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
A successful verification reactivates the endpoint and starts the replay. If the endpoint was permanently deactivated after the 12-hour window, re-enable it with PATCH .../{webhook_id} and {"is_active": true} — but note that its queued events were already dead-lettered by then.
Error Responses
Errors return detail as a message string. See Error Handling for the response shapes.
| Status | detail | Description |
|---|---|---|
| 400 | "Invalid webhook URL: ..." | The URL is not a reachable public https address |
| 400 | "Maximum of 10 webhooks per tenant reached" | The workspace already has 10 webhooks |
| 400 | "Cannot activate webhook until it is verified" | is_active: true on an unverified webhook |
| 400 | "Webhook must be verified before sending test events" | Test event on an unverified webhook |
| 400 | "Cannot retry delivery: ..." | The delivery can't be retried in its current state, or the webhook is disabled or unverified |
| 401 | "Invalid or expired API token" | Token is invalid or expired |
| 403 | "Missing required permission(s): ..." | Token lacks the required webhooks:* scope |
| 403 | message string | A field managed by a connected integration was changed on that integration's webhook |
| 404 | "Webhook endpoint not found", "Delivery not found" | Unknown ID, or it belongs to another workspace |
| 422 | list of field errors | Invalid request body, for example an unknown event type |
Next Steps
- Webhook Events Reference -- all available event types and payload formats
- Signature Verification -- secure your webhook endpoint