API Reference · Endpoints
Webhooks
Subscribe an HTTPS endpoint to Inboxi events, manage subscriptions, send test events and inspect delivery logs.
Webhooks push events to your server as they happen, so you do not have to poll. You can manage subscriptions here through the API or in Settings › Developer Hub › Webhooks. For a step-by-step setup see the Webhooks guide.
Every endpoint on this page needs the webhooks permission, except GET /api/v1/events, which any valid key can call.
Create a webhook
/api/v1/webhooksSubscribe a URL| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | required | Your HTTPS endpoint. |
events | string[] | optional | Event names to receive. Default ["*"] (everything). Unknown names are rejected. |
curl -X POST "https://inboxi.app/api/v1/webhooks" \
-H "X-API-Key: inx_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/inboxi/webhook",
"events": [
"message.delivered",
"message.read",
"message.failed"
]
}'Returns 201:
{
"success": true,
"data": {
"id": 7,
"url": "https://your-server.com/inboxi/webhook",
"events": ["message.delivered", "message.read", "message.failed"],
"secret": "sk_live_…",
"isActive": true,
"createdAt": "2026-03-18T12:00:00.000Z"
}
}sk_live_…), so you manage one credential. A secret you send in the body is ignored. Store the secret from this response: the list endpoint does not return it again.Errors: 400 "URL required"; 500 "Invalid URL format" or "Invalid events: …".
List webhooks
/api/v1/webhooksAll subscriptions with their healthcurl "https://inboxi.app/api/v1/webhooks" \
-H "X-API-Key: inx_live_your_api_key"{
"success": true,
"data": [
{
"id": 7,
"url": "https://your-server.com/inboxi/webhook",
"events": ["message.delivered", "message.read", "message.failed"],
"isActive": true,
"failureCount": 0,
"lastDeliveredAt": "2026-03-18T12:05:00.000Z",
"lastFailedAt": null,
"lastError": null,
"createdAt": "2026-03-18T12:00:00.000Z"
}
]
}Update a webhook
/api/v1/webhooks/:idChange the URL, events or active state| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | optional | New endpoint URL. |
events | string[] | optional | New event list (use ["*"] for all). |
isActive | boolean | optional | Pause (false) or resume (true). Resuming resets the failure counter and last error. |
curl -X PATCH "https://inboxi.app/api/v1/webhooks/7" \
-H "X-API-Key: inx_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"isActive": true
}'Returns the updated subscription in data, or 404 "Webhook not found".
Delete a webhook
/api/v1/webhooks/:idRemove a subscriptioncurl -X DELETE "https://inboxi.app/api/v1/webhooks/7" \
-H "X-API-Key: inx_live_your_api_key"{ "success": true }Send a test event
/api/v1/webhooks/:id/testDeliver a test.ping to the URLcurl -X POST "https://inboxi.app/api/v1/webhooks/7/test" \
-H "X-API-Key: inx_live_your_api_key"{ "success": true, "message": "Test webhook sent" }Your endpoint receives:
{
"event": "test.ping",
"timestamp": "2026-03-18T12:00:00.000Z",
"data": {
"message": "This is a test webhook from Inboxi",
"timestamp": "2026-03-18T12:00:00.000Z"
}
}The response confirms the attempt was made, not that your server accepted it. Check the delivery logs for the status code your endpoint returned.
Delivery logs
/api/v1/webhooks/:id/logsEvery delivery attempt, newest first| Parameter | Type | Required | Description |
|---|---|---|---|
page | number | optional | Page number. Default 1. |
limit | number | optional | Results per page. Default 50, max 100. |
curl "https://inboxi.app/api/v1/webhooks/7/logs?limit=20" \
-H "X-API-Key: inx_live_your_api_key"{
"success": true,
"data": [
{
"id": 5531,
"webhookSubscriptionId": 7,
"event": "message.delivered",
"payload": { "event": "message.delivered", "data": { ... } },
"responseStatus": 200,
"responseBody": "OK",
"success": true,
"duration": 182,
"attempt": 1,
"error": null,
"createdAt": "2026-03-18T12:05:00.000Z"
}
],
"pagination": { "total": 240, "page": 1, "limit": 20 }
}responseBody keeps the first 1,000 characters of your endpoint's reply.
Available events
/api/v1/eventsThe event names you can subscribe tocurl "https://inboxi.app/api/v1/events" \
-H "X-API-Key: inx_live_your_api_key"{
"success": true,
"events": [
{ "name": "message.received", "description": "New message received from a customer" },
{ "name": "message.sent", "description": "Message sent to a customer" },
...
]
}Subscribe to any of these names, or * for all. The last column shows which events Inboxi emits today; the others are accepted in subscriptions so you can register for them in advance.
| Event | Description | Emitted today |
|---|---|---|
message.received | New message received from a customer | - |
message.sent | Message sent to a customer | Yes, for messages sent through POST /api/v1/messages/send |
message.delivered | Message reached the customer's device | Yes, from WhatsApp delivery receipts |
message.read | Customer opened the message | Yes, from WhatsApp read receipts |
message.failed | Message could not be delivered (payload includes error.code and error.details) | Yes, from WhatsApp failure receipts |
conversation.created | New conversation started | - |
conversation.closed | Conversation was closed | - |
conversation.assigned | Conversation assigned to an agent | - |
contact.created | New contact created | - |
contact.updated | Contact info updated | - |
order.created | New order placed | - |
order.updated | Order status changed | - |
appointment.created | New appointment booked | - |
appointment.updated | Appointment status changed | - |
external_data.received | External data pushed via the API | Yes, after every external-data push |
test.ping | Test event for webhook verification | Yes, from the test endpoint |
Payload format
Every delivery is a POST with a JSON body and these headers:
| Header | Type | Required | Description |
|---|---|---|---|
Content-Type | string | optional | application/json |
X-Inboxi-Signature | string | optional | sha256=<hex>: HMAC-SHA256 of the raw body with your secret. |
X-Inboxi-Event | string | optional | The event name, for example message.delivered. |
X-Inboxi-Delivery | string | optional | A unique id (UUID) per delivery attempt. |
User-Agent | string | optional | Inboxi-Webhook/1.0 |
Events tied to a conversation (anything with a chatId) get a contact block that echoes the identity you supplied through the widget's identify(), so you can route the event into your own system without another API call:
{
"event": "message.failed",
"timestamp": "2026-03-18T12:00:03.000Z",
"data": {
"chatId": 42,
"messageId": 9876,
"whatsappMessageId": "wamid.HBgM…",
"to": "201012345678",
"platform": "whatsapp_cloud",
"status": "failed",
"timestamp": "2026-03-18T12:00:02.000Z",
"error": {
"code": 131026,
"title": "Message undeliverable",
"details": "…"
},
"contact": {
"chatId": 42,
"name": "Ahmed Hassan",
"phone": "201012345678",
"email": "ahmed@example.com",
"externalUserId": "vendor_abc_user_789",
"customData": { "plan": "pro", "vendorId": "abc" },
"baseVisitorId": null
}
}
}| Event | data fields |
|---|---|
message.delivered / read / failed | chatId, messageId, whatsappMessageId, to, platform, status, timestamp, error (failures only), contact |
message.sent | chatId, to, message, contact |
external_data.received | source, dataType, recordCount |
test.ping | message, timestamp |
Verify the signature
Compute the HMAC over the raw request bytes, before any JSON parsing; re-serializing the parsed body can change whitespace and break the hash. Compare in constant time, and reject payloads whose timestamp is more than a few minutes old.
const crypto = require('crypto');
const express = require('express');
const app = express();
// 1. Keep the raw body for signature checks
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function verifyWebhook(req, secret) {
const header = req.headers['x-inboxi-signature'] || '';
const [algo, signature] = header.split('='); // "sha256=abcd..."
if (algo !== 'sha256' || !signature) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(req.rawBody) // raw bytes, not JSON.stringify
.digest('hex');
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/inboxi/webhook', (req, res) => {
if (!verifyWebhook(req, process.env.INBOXI_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Replay protection: reject events older than 5 minutes
const age = Date.now() - new Date(req.body.timestamp).getTime();
if (Math.abs(age) > 5 * 60 * 1000) {
return res.status(401).send('Stale payload');
}
console.log('Event:', req.body.event);
res.status(200).send('OK');
});Retries and auto-disable
- 1Answer fast with a 2xxAny 2xx status within 10 seconds counts as delivered. Do heavy work after responding.
- 2Failed attempts are retriedA non-2xx answer, a timeout or a network error is retried up to 3 attempts in total, about 2 and 4 seconds apart.
- 3Repeated failures pause the webhookAfter 10 consecutive failed attempts the subscription is set to
isActive: false. Fix your endpoint, then resume it withPATCH /api/v1/webhooks/:idand{ "isActive": true }.
Deliveries can arrive more than once or out of order. Use X-Inboxi-Delivery and the timestamp to de-duplicate and order them.