Webhooks
Real-time event notifications | iMessage For Business
Webhooks allow you to receive real-time notifications when events occur in your Sendblue account. You can configure multiple webhooks for different event types and manage them via our API.
In this documentation, we will cover how to:
- Understand webhook types and formats
- Set up and manage webhooks
- Secure your webhook endpoints
- Handle webhook events
Webhook Types
Section titled “Webhook Types”The following webhook types are supported:
| Type | Description |
|---|---|
| receive | Triggered when you receive an inbound message |
| outbound | Triggered when an outbound message is sent |
| typing_indicator | Triggered when a contact starts or stops typing |
| call_log | Triggered for outbound calls placed from the dashboard. Inbound calls do not trigger this webhook. Use inbound_call instead. |
| inbound_call | Triggered when an inbound call to one of your Sendblue numbers ends. Fires for every call Sendblue routes for you, answered and missed alike, with the outcome in the payload. |
| line_blocked | Triggered when a line is blocked |
| line_assigned | Triggered when a line is assigned |
| contact_profile | Triggered when a Name & Photo profile update completes or fails |
| contact_created | Triggered when a contact is created |
Webhook Format
Section titled “Webhook Format”Webhooks can be specified in two formats:
- Simple URL string:
"https://example.com/webhook" - Object with URL and secret:
{ "url": "https://example.com/webhook", "secret": "my-secret" }
Managing Webhooks
Section titled “Managing Webhooks”Manage account webhook subscriptions in the Sendblue dashboard under Developer → Webhooks, or with the following API endpoints:
- List all webhooks - Get all configured webhooks
- Create webhooks - Add new webhooks (appends to existing)
- Update webhooks - Replace all webhooks
- Delete webhooks - Remove specific webhooks
Listing Webhooks
Section titled “Listing Webhooks”Retrieve all webhooks configured for your account:
curl -X GET https://api.sendblue.com/api/account/webhooks \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET"Response:
{ "status": "OK", "webhooks": { "receive": [ "https://example.com/webhook1", { "url": "https://example.com/webhook2", "secret": "webhook-secret" } ], "call_log": [], "inbound_call": [], "line_blocked": [], "line_assigned": [], "outbound": [], "contact_profile": [], "contact_created": ["https://example.com/contact-webhook"], "globalSecret": "global-secret" }}Adding Webhooks
Section titled “Adding Webhooks”Add new webhooks to your account. This endpoint appends to the existing webhook list:
curl -X POST https://api.sendblue.com/api/account/webhooks \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "webhooks": [ "https://example.com/new-webhook", { "url": "https://example.com/webhook-with-secret", "secret": "my-webhook-secret" } ], "type": "receive" }'Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| webhooks | array | Yes | Array of webhook URLs or webhook objects |
| type | string | No | Webhook type (default: receive) |
| globalSecret | string | No | Global secret to apply to all webhooks |
Replacing All Webhooks
Section titled “Replacing All Webhooks”Replace the entire webhook configuration for your account:
curl -X PUT https://api.sendblue.com/api/account/webhooks \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "webhooks": { "receive": ["https://example.com/webhook"], "call_log": ["https://example.com/call-webhook"], "contact_profile": ["https://example.com/contact-profile-webhook"], "contact_created": ["https://example.com/contact-webhook"], "globalSecret": "my-global-secret" } }'Deleting Webhooks
Section titled “Deleting Webhooks”Remove specific webhooks from your account:
curl -X DELETE https://api.sendblue.com/api/account/webhooks \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "webhooks": ["https://example.com/webhook-to-delete"], "type": "receive" }'Webhook Security
Section titled “Webhook Security”Sendblue supports multiple ways to secure your webhook endpoints:
- Per-webhook secret: Set a
secreton individual webhook objects - Global secret: Set a
globalSecretthat applies to all webhooks - Legacy secret: The
secretfield at the root level (older format)
When you configure a secret, Sendblue will include it in the webhook request headers, allowing you to verify that the request is genuinely from Sendblue.
// Example: Setting up a webhook with a secretconst response = await fetch("https://api.sendblue.com/api/account/webhooks", { method: "POST", headers: { "sb-api-key-id": "YOUR_API_KEY", "sb-api-secret-key": "YOUR_API_SECRET", "Content-Type": "application/json", }, body: JSON.stringify({ webhooks: [ { url: "https://myapp.com/webhooks/sendblue", secret: "my-secure-secret-123", }, ], type: "receive", }),});Handling Webhooks
Section titled “Handling Webhooks”Inbound Message Webhook
Section titled “Inbound Message Webhook”When you receive an inbound message, Sendblue will POST to your configured receive webhook. This example payload is an inline reply:
{ "accountEmail": "your-account-email", "content": "Hello!", "is_outbound": false, "status": "RECEIVED", "error_code": null, "error_message": null, "error_reason": null, "message_handle": "99DCC379-DD76-4712-BA65-11EFB33B8CD6", "date_sent": "2025-12-12T15:41:20.932Z", "date_updated": "2025-12-12T15:41:20.995Z", "from_number": "+19998887777", "number": "+19998887777", "to_number": "+15122164639", "was_downgraded": null, "plan": "dedicated", "media_url": "", "message_type": "message", "group_id": "", "participants": ["+19998887777", "+15122164639"], "send_style": "", "opted_out": false, "error_detail": null, "sendblue_number": "+15122164639", "service": "iMessage", "group_display_name": null, "reply_to": { "message_handle": "2F172CD8-C082-4A54-B9B3-D5B78CF4D0C3" }, "thread_originator": { "message_handle": "2F172CD8-C082-4A54-B9B3-D5B78CF4D0C3" }}Outbound Message Webhook
Section titled “Outbound Message Webhook”For outbound messages, you can use the status_callback parameter when sending a message, or configure an outbound webhook to receive all outbound message status updates. This example is an inline reply:
{ "accountEmail": "your-account-email", "content": "Hello world!", "is_outbound": true, "status": "SENT", "error_code": null, "error_message": null, "error_reason": null, "message_handle": "5a17319e-cbcf-443e-897e-d8b0c04b1b09", "date_sent": "2025-12-12T15:35:35.410Z", "date_updated": "2025-12-12T15:35:35.410Z", "from_number": "+18649820355", "number": "+19998887777", "to_number": "+19998887777", "was_downgraded": null, "plan": "dedicated", "media_url": "", "message_type": "message", "group_id": "", "participants": [], "send_style": "", "opted_out": false, "error_detail": null, "sendblue_number": null, "service": "iMessage", "group_display_name": null, "reply_to": { "message_handle": "99DCC379-DD76-4712-BA65-11EFB33B8CD6", "part_index": 0 }, "thread_originator": { "message_handle": "99DCC379-DD76-4712-BA65-11EFB33B8CD6" }}Inline replies include reply_to and thread_originator objects. Each contains
a message_handle; responses may also include server-reported part metadata
when it is authoritative. These objects are omitted for messages outside a
reply thread. See the inline replies guide for field
semantics and send examples.
Call Log Webhook
Section titled “Call Log Webhook”When an outbound call is placed from the Sendblue dashboard, Sendblue will POST to your configured call_log webhook with the following payload. Inbound calls are not logged to the call-log pipeline and do not trigger the call_log webhook.
{ "event_type": "call_log", "call_id": "cs_abc123def456", "from_number": "+15551234567", "to_number": "+15559876543", "direction": "outbound", "status": "COMPLETED", "duration": 120, "provider": "twilio", "company_id": "550e8400-e29b-41d4-a716-446655440000", "contact_id": "660e8400-e29b-41d4-a716-446655440000", "recording_url": "https://api.twilio.com/recordings/RE1234.mp3", "transcript": "Hello, how can I help you today?", "metadata": {}, "created_at": "2026-02-07T12:00:00Z", "disposition": "connected", "origin": "DASHBOARD",}Call Log Payload Fields
Section titled “Call Log Payload Fields”| Field | Type | Description |
|---|---|---|
| event_type | string | Always "call_log" |
| call_id | string | Sendblue call session identifier |
| from_number | string | E.164 formatted caller phone number |
| to_number | string | E.164 formatted called phone number |
| direction | string | "outbound" for webhook deliveries |
| status | string | Final status of the call (e.g. COMPLETED, CANCELLED) |
| duration | integer | Call duration in seconds (null if not available) |
| provider | string | Telephony provider ("twilio" or "facetime") |
| company_id | string | UUID of the company |
| contact_id | string | UUID of the contact (null if not linked) |
| recording_url | string | URL of the call recording (null if not recorded) |
| transcript | string | Call transcript (null if not transcribed) |
| metadata | object | Additional metadata associated with the call |
| created_at | string | ISO 8601 timestamp of when the call was created |
| disposition | string | Call disposition (e.g. connected, not_answered, voicemail) |
| origin | string | Where the call originated from (e.g. DASHBOARD) |
| sent_by | string | Identifier of the user who initiated the call |
Inbound Call Webhook
Section titled “Inbound Call Webhook”When someone calls one of your Sendblue numbers, Sendblue POSTs to your configured inbound_call webhook once the call ends. It fires for every call Sendblue routes for you. Forwarded to your forwarding number, or answered in the dashboard, same goes for missed ones. Use the status and answered fields to tell them apart, for example to follow up on missed calls.
{ "event_type": "inbound_call", "event_id": "inbound-call-CA1234567890abcdef1234567890abcdef", "accountEmail": "your-account-email", "call_id": "CA1234567890abcdef1234567890abcdef", "from_number": "+15551234567", "to_number": "+15559876543", "forwarded_to": "+15550001111", "direction": "inbound", "status": "NO_ANSWER", "answered": false, "duration": null, "provider": "twilio", "company_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-08-12T18:00:00.000Z", "ended_at": "2026-08-12T18:00:35.000Z"}Inbound Call Payload Fields
Section titled “Inbound Call Payload Fields”| Field | Type | Description |
|---|---|---|
| event_type | string | Always "inbound_call" |
| event_id | string | Unique identifier for this event: use it to deduplicate deliveries |
| accountEmail | string | Identifier of the associated account. Usually an email address, but for accounts created through the dashboard it is the company name |
| call_id | string | Provider call identifier for the inbound call |
| from_number | string | E.164 formatted number of the caller |
| to_number | string | E.164 formatted Sendblue number that was called |
| forwarded_to | string | Number the call was forwarded to (null if not forwarded) |
| direction | string | Always "inbound" |
| status | string | Final outcome of the call: COMPLETED, NO_ANSWER, BUSY, FAILED, or CANCELLED |
| answered | boolean | Convenience flag: true when the call was answered (status is COMPLETED), false for missed calls |
| duration | integer | Talk time in seconds (null when the call was never answered) |
| provider | string | Telephony provider that carried the call ("twilio" or "telnyx") |
| company_id | string | UUID of the company (null if not linked) |
| created_at | string | ISO 8601 timestamp of when the inbound call started |
| ended_at | string | ISO 8601 timestamp of when the final outcome was recorded. Normally within seconds of the call ending, but later when a provider callback was lost and the outcome had to be recovered |
Contact Profile Webhook
Section titled “Contact Profile Webhook”Subscribe to contact_profile before calling
POST /api/v2/contact-sharing/profile to receive the final result of a profile
update. The profile request returns immediately; the webhook is sent later when
the update completes or fails.
curl -X POST "https://api.sendblue.com/api/account/webhooks" \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "webhooks": ["https://example.com/webhooks/contact-profile"], "type": "contact_profile" }'Successful update:
{ "event_type": "contact_profile.completed", "event_id": "contact-profile:bc0371a7-b615-4d49-9e42-fec74da98a53:completed", "request_id": "3f377d6e-2d36-45bb-b7ed-e87f0b7e792d", "operation_id": "bc0371a7-b615-4d49-9e42-fec74da98a53", "accountEmail": "your-account-email", "from_number": "+14155551234", "status": "completed", "profile": { "hasProfile": true, "sharingEnabled": true, "firstName": "Ada", "lastName": "Lovelace", "displayName": "Ada Lovelace", "hasPhoto": true, "publishRecordPresent": true }, "error_code": null, "error_message": null, "requested_at": "2026-08-24T15:00:00.000Z", "observed_at": "2026-08-24T15:01:00.000Z"}| Field | Type | Description |
|---|---|---|
event_type | string | contact_profile.completed or contact_profile.failed |
event_id | string | Stable event identifier. Use it to deduplicate deliveries. |
request_id | string | Identifier for the accepted API request |
operation_id | string or null | Identifier for the profile update |
accountEmail | string | Account associated with the Sendblue number |
from_number | string | Sendblue number whose profile was updated |
status | string | completed or failed |
profile | object or null | Final profile state for a completed update; null for a failed update |
error_code | string or null | Machine-readable failure code |
error_message | string or null | Failure description |
requested_at | string | ISO 8601 time when the update was requested |
observed_at | string | ISO 8601 time when the final result was observed |
For failed updates, event_type is contact_profile.failed, status is
failed, profile is null, and the error fields describe the failure.
Delivery is at least once. Deduplicate deliveries using event_id. Repeated
requests for the same in-progress update may produce a single completion event.
The event confirms that the sender’s profile was updated; it does not confirm
when a recipient sees or accepts it.
Returning 410 Gone does not automatically remove a contact_profile
subscription. Remove it with the webhook API when it is no longer needed.
Message Webhook Payload Fields
Section titled “Message Webhook Payload Fields”| Field | Type | Description |
|---|---|---|
| accountEmail | string | Associated account email |
| content | string | Message content |
| is_outbound | boolean | True if message is sent, false if message is received |
| status | string | The current status of the message |
| error_code | int | Error code (null if no error) |
| error_message | string | Descriptive error message (null if no error) |
| error_reason | string | Additional error context (null if no error) |
| error_detail | string | Detailed error information (null if no error) |
| message_handle | string | Sendblue message handle |
| date_sent | string | ISO 8601 formatted date string of when message was created |
| date_updated | string | ISO 8601 formatted date string of when message was last updated |
| from_number | string | E.164 formatted phone number of the message dispatcher |
| number | string | E.164 formatted phone number of your end-user |
| to_number | string | E.164 formatted phone number of the message recipient |
| was_downgraded | boolean | True if the end user does not support iMessage, null otherwise |
| plan | string | Value of the Sendblue account plan |
| media_url | string | A CDN link to any media attached to the message |
| message_type | string | Type of message (e.g., “message”) |
| group_id | string | Group identifier (empty string for non-group messages) |
| participants | array | Array of participant phone numbers |
| send_style | string | Expressive message style if used (empty string if none) |
| opted_out | boolean | True if the recipient has opted out |
| sendblue_number | string | The Sendblue phone number used |
| service | string | The messaging service used (e.g., “iMessage”, “SMS”) |
| group_display_name | string | Display name for group chats (null for non-group messages) |
| sender_email | string | Email of the seat (user) that sent the message. Auto-populated when a seat_id is supplied on the send endpoint; null otherwise. |
| seat_id | string | UUID of the seat that sent the message (null when no seat was provided). |
Best Practices
Section titled “Best Practices”1. Use HTTPS Only
Section titled “1. Use HTTPS Only”All webhook URLs must use HTTPS to ensure secure communication.
2. Implement Idempotency
Section titled “2. Implement Idempotency”Your webhook endpoints should be idempotent, as they may receive duplicate events.
Use message_handle for message events and event_id for events that provide it.
// Example: Idempotent webhook handlerconst processedMessages = new Set();
app.post("/webhook", (req, res) => { const { message_handle } = req.body;
if (processedMessages.has(message_handle)) { return res.status(200).send("Already processed"); }
processedMessages.add(message_handle); // Process the webhook... res.status(200).send("OK");});3. Verify Webhook Signatures
Section titled “3. Verify Webhook Signatures”Always verify that webhook requests are coming from Sendblue by checking the secret in the request headers.
4. Return Appropriate Status Codes
Section titled “4. Return Appropriate Status Codes”- Return 200-299 for successful processing
- Return 410 Gone if you want Sendblue to automatically remove the webhook.
This automatic removal does not apply to
contact_profilewebhooks.
5. Handle Errors Gracefully
Section titled “5. Handle Errors Gracefully”Implement proper error handling and logging in your webhook endpoints to troubleshoot issues.
Error Responses
Section titled “Error Responses”401 Unauthorized
Section titled “401 Unauthorized”{ "status": "ERROR", "message": "Unauthorized"}Authentication failed. Check your API credentials.
400 Bad Request
Section titled “400 Bad Request”{ "status": "ERROR", "message": "Missing or invalid webhooks array"}The request body is malformed or missing required fields.
500 Internal Server Error
Section titled “500 Internal Server Error”{ "status": "ERROR", "message": "Error message details"}An internal error occurred. Contact support if this persists.
Examples
Section titled “Examples”Example 1: Basic Webhook Setup
Section titled “Example 1: Basic Webhook Setup”// Add a simple webhook for receiving messagesconst response = await fetch("https://api.sendblue.com/api/account/webhooks", { method: "POST", headers: { "sb-api-key-id": "YOUR_API_KEY", "sb-api-secret-key": "YOUR_API_SECRET", "Content-Type": "application/json", }, body: JSON.stringify({ webhooks: ["https://myapp.com/webhooks/sendblue"], type: "receive", }),});
const data = await response.json();console.log(data);Example 2: Multi-Type Webhook Configuration
Section titled “Example 2: Multi-Type Webhook Configuration”// Configure webhooks for multiple event typesconst response = await fetch("https://api.sendblue.com/api/account/webhooks", { method: "PUT", headers: { "sb-api-key-id": "YOUR_API_KEY", "sb-api-secret-key": "YOUR_API_SECRET", "Content-Type": "application/json", }, body: JSON.stringify({ webhooks: { receive: ["https://myapp.com/webhooks/receive"], outbound: ["https://myapp.com/webhooks/outbound"], call_log: ["https://myapp.com/webhooks/calls"], inbound_call: ["https://myapp.com/webhooks/inbound-calls"], contact_profile: ["https://myapp.com/webhooks/contact-profile"], contact_created: ["https://myapp.com/webhooks/contacts"], globalSecret: "my-global-secret", }, }),});Example 3: Express.js Webhook Handler
Section titled “Example 3: Express.js Webhook Handler”const express = require("express");const app = express();
app.use(express.json());
app.post("/webhooks/sendblue", (req, res) => { const { content, from_number, message_handle, is_outbound, status } = req.body;
console.log(`New message from ${from_number}: ${content}`); console.log(`Message handle: ${message_handle}`); console.log(`Status: ${status}`);
// Process the message here // ...
// Always respond with 200 to acknowledge receipt res.status(200).send("OK");});
app.listen(3000, () => { console.log("Webhook server running on port 3000");});Retry Policy
Section titled “Retry Policy”Sendblue retries webhook delivery up to 3 times if your endpoint returns a 5xx server error. Sendblue waits 45 seconds for a response from your endpoint. If no response is received within that window, the delivery is treated as failed and will be retried.
Ensure your endpoint is idempotent to handle potential duplicate deliveries
during retries. Use message_handle for message events and event_id for
events that provide it.
Limitations
Section titled “Limitations”- Account-level only: Webhook URLs are configured at the account level. All lines on a single account share the same webhook endpoints. You cannot set a different webhook per phone number.
- No default status callback: There is no way to set a global default
status_callbackwebhook. The status callback URL must be specified on each individual message send request. See Sending messages. - Call log scope: The
call_logwebhook only fires for outbound calls made from the dashboard. Inbound calls trigger theinbound_callwebhook instead. - Calls Sendblue never routes: an inbound call that is turned away before it reaches you produces no
inbound_callwebhook, for example, a caller on your block list, or a call arriving while your account has no forwarding number configured. Configure a forwarding number if you want missed calls reported. - Contact update webhooks: Webhook events for contact updates (e.g., tags and notes) are not currently available. This is being evaluated for a future release.
- Webhook error logging: Dedicated webhook error logging is coming soon and will be accessible for internal tracking and debugging.
Additional Notes
Section titled “Additional Notes”- Webhook URLs are automatically validated to ensure they are valid HTTPS URLs
- The API maintains backward compatibility with legacy webhook formats
- The
receivewebhook type is the most commonly used for inbound messages - You can configure multiple webhooks for the same event type for redundancy