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.

Event in Inboxi
e.g. a message is delivered
matching subscriptions
Inboxi
Signs the JSON body with your secret
POST + X-Inboxi-Signature
Your endpoint
Verifies the signature, answers 2xx

Every endpoint on this page needs the webhooks permission, except GET /api/v1/events, which any valid key can call.

Create a webhook

POST/api/v1/webhooksSubscribe a URL
ParameterTypeRequiredDescription
urlstringrequiredYour HTTPS endpoint.
eventsstring[]optionalEvent 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:

json
{
  "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"
  }
}
The signing secret is your API key secret
Inboxi signs deliveries with the secret of your workspace's newest active API key (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

GET/api/v1/webhooksAll subscriptions with their health
curl "https://inboxi.app/api/v1/webhooks" \
  -H "X-API-Key: inx_live_your_api_key"
json
{
  "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

PATCH/api/v1/webhooks/:idChange the URL, events or active state
ParameterTypeRequiredDescription
urlstringoptionalNew endpoint URL.
eventsstring[]optionalNew event list (use ["*"] for all).
isActivebooleanoptionalPause (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

DELETE/api/v1/webhooks/:idRemove a subscription
curl -X DELETE "https://inboxi.app/api/v1/webhooks/7" \
  -H "X-API-Key: inx_live_your_api_key"
json
{ "success": true }

Send a test event

POST/api/v1/webhooks/:id/testDeliver a test.ping to the URL
curl -X POST "https://inboxi.app/api/v1/webhooks/7/test" \
  -H "X-API-Key: inx_live_your_api_key"
json
{ "success": true, "message": "Test webhook sent" }

Your endpoint receives:

json
{
  "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

GET/api/v1/webhooks/:id/logsEvery delivery attempt, newest first
ParameterTypeRequiredDescription
pagenumberoptionalPage number. Default 1.
limitnumberoptionalResults 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"
json
{
  "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

GET/api/v1/eventsThe event names you can subscribe to
curl "https://inboxi.app/api/v1/events" \
  -H "X-API-Key: inx_live_your_api_key"
json
{
  "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.

EventDescriptionEmitted today
message.receivedNew message received from a customer-
message.sentMessage sent to a customerYes, for messages sent through POST /api/v1/messages/send
message.deliveredMessage reached the customer's deviceYes, from WhatsApp delivery receipts
message.readCustomer opened the messageYes, from WhatsApp read receipts
message.failedMessage could not be delivered (payload includes error.code and error.details)Yes, from WhatsApp failure receipts
conversation.createdNew conversation started-
conversation.closedConversation was closed-
conversation.assignedConversation assigned to an agent-
contact.createdNew contact created-
contact.updatedContact info updated-
order.createdNew order placed-
order.updatedOrder status changed-
appointment.createdNew appointment booked-
appointment.updatedAppointment status changed-
external_data.receivedExternal data pushed via the APIYes, after every external-data push
test.pingTest event for webhook verificationYes, from the test endpoint

Payload format

Every delivery is a POST with a JSON body and these headers:

HeaderTypeRequiredDescription
Content-Typestringoptionalapplication/json
X-Inboxi-Signaturestringoptionalsha256=<hex>: HMAC-SHA256 of the raw body with your secret.
X-Inboxi-EventstringoptionalThe event name, for example message.delivered.
X-Inboxi-DeliverystringoptionalA unique id (UUID) per delivery attempt.
User-AgentstringoptionalInboxi-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:

json
{
  "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
    }
  }
}
Eventdata fields
message.delivered / read / failedchatId, messageId, whatsappMessageId, to, platform, status, timestamp, error (failures only), contact
message.sentchatId, to, message, contact
external_data.receivedsource, dataType, recordCount
test.pingmessage, 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

  1. 1
    Answer fast with a 2xx
    Any 2xx status within 10 seconds counts as delivered. Do heavy work after responding.
  2. 2
    Failed attempts are retried
    A non-2xx answer, a timeout or a network error is retried up to 3 attempts in total, about 2 and 4 seconds apart.
  3. 3
    Repeated failures pause the webhook
    After 10 consecutive failed attempts the subscription is set to isActive: false. Fix your endpoint, then resume it with PATCH /api/v1/webhooks/:id and { "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.