Webhooks let you receive real-time notifications when events occur in your Texting Blue account. Instead of polling the API, configure a webhook URL and Texting Blue will send an HTTP POST request to your server whenever a subscribed event fires.
| Event | Description |
|---|---|
message.received |
An inbound iMessage was received on one of your numbers. |
message.sent |
An outbound message was successfully sent by your device. |
message.delivered |
An outbound message was confirmed delivered to the recipient. |
message.failed |
An outbound message failed to deliver. |
Every webhook delivery is an HTTP POST with a JSON body:
{
"id": "evt_xxxxxxxxxxxx",
"type": "message.received",
"timestamp": "2026-02-07T12:00:00Z",
"data": {
"id": "msg_xxxxxxxxxxxx",
"from": "+14155551234",
"to": "+14155559876",
"content": "Hey, is this available?",
"media_url": null,
"received_at": "2026-02-07T12:00:00Z"
}
}| Header | Description |
|---|---|
Content-Type |
application/json |
x-textingblue-signature |
HMAC-SHA256 signature for verification. Format: sha256={hex_digest}. |
x-textingblue-event |
The event type (e.g., message.received). |
If your endpoint does not return a 2xx response, Texting Blue retries with the following schedule:
| Attempt | Delay |
|---|---|
| 1st retry | Immediate |
| 2nd retry | 10 seconds |
| 3rd retry | 60 seconds |
| 4th retry | 300 seconds (5 minutes) |
After 10 consecutive failures, the webhook is automatically disabled. You can re-enable it by updating the webhook's active field to true.
POST /v1/webhooks
Permission: webhooks:manage
Register a new webhook endpoint. The response includes a secret field -- save it immediately, as it is only shown once. You will use this secret to verify webhook signatures.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | The HTTPS URL to receive webhook payloads. |
events |
string[] | Yes | List of event types to subscribe to. |
phone_number |
string | No | Filter events to a specific phone number (E.164). If omitted, receives events for all numbers. |
{
"id": "whk_xxxxxxxxxxxx",
"url": "https://example.com/webhooks/texting-blue",
"events": ["message.received", "message.failed"],
"phone_number": null,
"active": true,
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"failure_count": 0,
"last_triggered_at": null,
"created_at": "2026-02-07T12:00:00Z"
}Important: The
secretfield is only included in the create response. Store it securely. If you lose it, delete the webhook and create a new one.
curl -X POST https://api.texting.blue/v1/webhooks \
-H "x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/texting-blue",
"events": ["message.received", "message.failed"]
}'const response = await fetch("https://api.texting.blue/v1/webhooks", {
method: "POST",
headers: {
"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/webhooks/texting-blue",
events: ["message.received", "message.failed"],
}),
});
const webhook = await response.json();
// Save webhook.secret securely -- it is only shown once
console.log(webhook.secret); // "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"import requests
response = requests.post(
"https://api.texting.blue/v1/webhooks",
headers={
"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"Content-Type": "application/json",
},
json={
"url": "https://example.com/webhooks/texting-blue",
"events": ["message.received", "message.failed"],
},
)
webhook = response.json()
# Save webhook["secret"] securely -- it is only shown once
print(webhook["secret"]) # "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"$ch = curl_init("https://api.texting.blue/v1/webhooks");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"url" => "https://example.com/webhooks/texting-blue",
"events" => ["message.received", "message.failed"],
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$webhook = json_decode($response, true);
// Save $webhook["secret"] securely -- it is only shown once
echo $webhook["secret"]; // "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"GET /v1/webhooks
Permission: webhooks:manage
Returns all webhooks registered on your account. The secret field is not included in list responses.
{
"webhooks": [
{
"id": "whk_xxxxxxxxxxxx",
"url": "https://example.com/webhooks/texting-blue",
"events": ["message.received", "message.failed"],
"active": true,
"failure_count": 0,
"last_triggered_at": "2026-02-07T11:55:00Z",
"created_at": "2026-02-07T12:00:00Z"
}
]
}curl -X GET https://api.texting.blue/v1/webhooks \
-H "x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"const response = await fetch("https://api.texting.blue/v1/webhooks", {
headers: {
"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
},
});
const { webhooks } = await response.json();import requests
response = requests.get(
"https://api.texting.blue/v1/webhooks",
headers={"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"},
)
webhooks = response.json()["webhooks"]$ch = curl_init("https://api.texting.blue/v1/webhooks");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$webhooks = json_decode($response, true)["webhooks"];GET /v1/webhooks/:id
Permission: webhooks:manage
Retrieve details of a single webhook.
{
"id": "whk_xxxxxxxxxxxx",
"url": "https://example.com/webhooks/texting-blue",
"events": ["message.received", "message.failed"],
"active": true,
"failure_count": 0,
"last_triggered_at": "2026-02-07T11:55:00Z",
"created_at": "2026-02-07T12:00:00Z"
}curl -X GET https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx \
-H "x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"PUT /v1/webhooks/:id
Permission: webhooks:manage
Update a webhook's URL, events, or active status. Only include the fields you want to change.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | No | New HTTPS URL for the webhook. |
events |
string[] | No | New list of event types to subscribe to. |
active |
boolean | No | Set to false to disable or true to re-enable. |
Returns the updated webhook object.
{
"id": "whk_xxxxxxxxxxxx",
"url": "https://example.com/webhooks/v2",
"events": ["message.received", "message.sent", "message.delivered", "message.failed"],
"active": true,
"failure_count": 0,
"last_triggered_at": "2026-02-07T11:55:00Z",
"created_at": "2026-02-07T12:00:00Z"
}curl -X PUT https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx \
-H "x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/v2",
"events": ["message.received", "message.sent", "message.delivered", "message.failed"]
}'const response = await fetch(
"https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx",
{
method: "PUT",
headers: {
"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"Content-Type": "application/json",
},
body: JSON.stringify({
events: [
"message.received",
"message.sent",
"message.delivered",
"message.failed",
],
}),
}
);
const webhook = await response.json();import requests
response = requests.put(
"https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx",
headers={
"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"Content-Type": "application/json",
},
json={
"events": [
"message.received",
"message.sent",
"message.delivered",
"message.failed",
]
},
)
webhook = response.json()$ch = curl_init("https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"events" => [
"message.received",
"message.sent",
"message.delivered",
"message.failed",
],
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$webhook = json_decode($response, true);If a webhook is automatically disabled after 10 consecutive failures, fix the underlying issue on your server and then re-enable it:
curl -X PUT https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx \
-H "x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-H "Content-Type: application/json" \
-d '{ "active": true }'DELETE /v1/webhooks/:id
Permission: webhooks:manage
Permanently remove a webhook. Any in-flight deliveries will still be attempted, but no new events will be sent.
{
"deleted": true
}curl -X DELETE https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx \
-H "x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"const response = await fetch(
"https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx",
{
method: "DELETE",
headers: {
"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
},
}
);
const { deleted } = await response.json();import requests
response = requests.delete(
"https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx",
headers={"x-api-key": "tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"},
)
print(response.json()["deleted"]) # True$ch = curl_init("https://api.texting.blue/v1/webhooks/whk_xxxxxxxxxxxx");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-api-key: tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data["deleted"]; // 1 (true)