Webhooks
Webhooks let you receive real-time HTTP notifications when message events occur -- incoming messages, delivery confirmations, and failures.
Event types
| Event | Description |
|---|---|
message.received | An inbound iMessage was received on your connected number |
message.sent | An outbound message was successfully sent from your iPhone |
message.delivered | A sent message was confirmed as delivered. This functionality is working only with Standard plan and higher. |
message.failed | A message failed to send |
Create a webhook
POST /v1/webhooks
Required permission: webhooks:manage
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive webhook events |
events | string[] | Yes | Array of event types to subscribe to |
phone_number | string | No | Filter events to a specific phone number |
Example
- cURL
- JavaScript
- Python
curl -X POST https://api.texting.blue/v1/webhooks \
-H "Content-Type: application/json" \
-H "x-api-key: tb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{
"url": "https://yourapp.com/webhook",
"events": ["message.received", "message.sent", "message.failed"]
}'
const response = await fetch('https://api.texting.blue/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'tb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
body: JSON.stringify({
url: 'https://yourapp.com/webhook',
events: ['message.received', 'message.sent', 'message.failed'],
}),
});
const webhook = await response.json();
console.log(webhook.secret); // Save this -- shown only once
import requests
response = requests.post(
'https://api.texting.blue/v1/webhooks',
headers={
'Content-Type': 'application/json',
'x-api-key': 'tb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
json={
'url': 'https://yourapp.com/webhook',
'events': ['message.received', 'message.sent', 'message.failed'],
},
)
webhook = response.json()
print(webhook['secret']) # Save this -- shown only once
Response
Status: 201 Created
{
"id": "wh_xxxxxxxxxxxx",
"url": "https://yourapp.com/webhook",
"events": ["message.received", "message.sent", "message.failed"],
"phone_number": null,
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"active": true,
"created_at": "2026-02-07T12:00:00Z"
}
Save the secret value immediately. It's only shown when the webhook is created and is required for signature verification.
Webhook payload
When an event fires, Texting Blue sends an HTTP POST request to your webhook URL.
Headers
| Header | Description |
|---|---|
Content-Type | application/json |
x-textingblue-signature | HMAC-SHA256 signature of the request body |
x-textingblue-event | The event type (e.g., message.received) |
Body
{
"id": "evt_xxxxxxxxxxxx",
"type": "message.received",
"timestamp": "2026-02-07T12:00:00Z",
"data": {
"id": "msg_xxxxxxxxxxxx",
"from": "+14155551234",
"to": "+14155559876",
"content": "Hey, got your message!",
"media_url": null,
"received_at": "2026-02-07T12:00:00Z"
}
}
Signature verification
Every webhook request includes an x-textingblue-signature header containing an HMAC-SHA256 signature. Always verify this signature to ensure the request came from Texting Blue.
The signature format is: sha256={hex_digest}
- Node.js
- Python
import crypto from 'crypto';
function verifyWebhookSignature(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your Express handler:
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-textingblue-signature'];
const isValid = verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process the event...
res.json({ received: true });
});
import hmac
import hashlib
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your Flask handler:
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('x-textingblue-signature', '')
is_valid = verify_webhook_signature(
request.data, signature, WEBHOOK_SECRET
)
if not is_valid:
return jsonify({'error': 'Invalid signature'}), 401
event = request.json
# Process the event...
return jsonify({'received': True})
List webhooks
GET /v1/webhooks
Returns all webhooks for your team.
curl https://api.texting.blue/v1/webhooks \
-H "x-api-key: tb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response
{
"webhooks": [
{
"id": "wh_xxxxxxxxxxxx",
"url": "https://yourapp.com/webhook",
"events": ["message.received", "message.sent"],
"active": true,
"failure_count": 0,
"last_triggered_at": "2026-02-07T11:55:00Z",
"created_at": "2026-02-01T10:00:00Z"
}
]
}
Update a webhook
PUT /v1/webhooks/:id
Update the URL, events, or active status of a webhook.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | No | New webhook URL |
events | string[] | No | New event types to subscribe to |
active | boolean | No | Enable or disable the webhook |
curl -X PUT https://api.texting.blue/v1/webhooks/wh_xxxxxxxxxxxx \
-H "Content-Type: application/json" \
-H "x-api-key: tb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{"active": false}'
Delete a webhook
DELETE /v1/webhooks/:id
Permanently remove a webhook.
curl -X DELETE https://api.texting.blue/v1/webhooks/wh_xxxxxxxxxxxx \
-H "x-api-key: tb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response
{
"deleted": true
}
Retry behavior
If your webhook endpoint returns an error (HTTP status >= 400) or times out, Texting Blue retries the delivery:
- Attempt 1: Immediate
- Attempt 2: After 10 seconds
- Attempt 3: After 60 seconds
- Attempt 4: After 300 seconds (5 minutes)
After 10 consecutive failures across all deliveries, the webhook is automatically disabled. You'll need to re-enable it manually via the API or dashboard after fixing the issue.
Best practices
- Always verify webhook signatures before processing events
- Return a
200response quickly (within 5 seconds). Do heavy processing asynchronously. - Use a unique URL path per webhook for easier debugging
- Monitor your webhook's
failure_countin the dashboard - Handle duplicate events gracefully (use the
idfield for deduplication)
Next steps
- Tutorial: Set up webhooks for incoming messages
- Team management API -- Manage your team