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 30 seconds
- Accept POST requests with a JSON body
- Be publicly accessible from the internet
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 | Request timeout in seconds (default: 30, 5-60) |
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 30s), 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": 30,
"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 |
| 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
| Status | Error Code | Description |
|---|---|---|
| 401 | INVALID_TOKEN | Token is invalid or expired |
| 403 | INSUFFICIENT_SCOPE | Token lacks required webhooks scope |
| 404 | RESOURCE_NOT_FOUND | Webhook ID does not exist or belongs to another tenant |
| 422 | VALIDATION_ERROR | Invalid URL or events list |
Next Steps
- Webhook Events Reference -- all available event types and payload formats
- Signature Verification -- secure your webhook endpoint