InboxNest webhooks
Get a signed POST when a message arrives, a reply is sent, an email bounces or a conversation changes, and verify the signature in Node, Python or PHP.
Webhooks tell your server when something happens in InboxNest, so you don’t have to poll. InboxNest sends a signed JSON POST to your endpoint for each event you subscribe to.
Endpoints
Add endpoints in Settings → Webhooks, or with the API (POST /webhooks, scope webhooks:write):
{"url": "https://example.com/inboxnest", "events": ["message.received", "email.bounced"], "domain_ids": ["3"], "description": "Support bot"}
The response includes the signing secret (whsec_…). It is shown only once; use Roll secret (or POST /webhooks/{id}/roll-secret) to get a new one. An account can have up to 5 endpoints. Leave out domain_ids to get events for every domain.
Endpoints must use https:// and point to a public internet address. Private, local and cloud-metadata addresses are refused, both when you add the endpoint and at every delivery. Redirects aren’t followed. InboxNest waits up to 10 seconds and reads at most 4 KB of your response.
Events
message.received: A customer message arrived (after spam filtering; data.spam says whether it went to Spam).message.sent: A reply or new conversation was accepted by the mail provider.message.delivery_failed: An outbound message hard-bounced.message.complained: A recipient marked an outbound message as spam.conversation.updated: A conversation moved folder, was read, or had its follow-up flag changed.address.created: An address was added to a domain.address.removed: An address was removed from a domain.draft.created: A reply draft was saved on a conversation.email.sent: A transactional email was accepted by the mail provider.email.delivered: A transactional email was delivered.email.bounced: A transactional email bounced.email.complained: A recipient marked a transactional email as spam.email.failed: A transactional email could not be sent.
Payload
{
"id": "evt_3fJ2kq9XbW1nT7yLmP0aZc4d",
"type": "message.received",
"created_at": "2026-11-10T09:12:03Z",
"account_id": "42",
"data": {
"conversation_id": "1234",
"message_id": "5501",
"domain": "acme.com",
"address": "support@acme.com",
"from": "jo@example.com",
"subject": "Where is my order?",
"text_preview": "Hi, order #881 hasn't arrived…",
"spam": false,
"new_conversation": true
}
}
Message events carry a preview of at most 500 characters, never the full body. Fetch the conversation with GET /conversations/{id} when you need the text. The id is unique per event, so use it to ignore duplicates. Each request also has InboxNest-Event-Id and InboxNest-Event-Type headers.
Verifying signatures
Every delivery has an InboxNest-Signature header: t=<unix time>,v1=<hex>. v1 is the HMAC-SHA256 of t + "." + raw request body, keyed with your endpoint secret. Check it against the raw body before you parse the JSON, compare in constant time, and reject old timestamps (for example, more than 5 minutes) to stop replays.
Node
import crypto from 'node:crypto';
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds;
return fresh && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
// Express: app.post('/inboxnest', express.raw({ type: 'application/json' }), (req, res) => { … verify(req.body.toString(), req.get('InboxNest-Signature'), secret) … })
Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return abs(time.time() - int(parts["t"])) <= tolerance and hmac.compare_digest(expected, parts["v1"])
PHP
function inboxnest_verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return abs(time() - (int) $parts['t']) <= $tolerance && hash_equals($expected, (string) ($parts['v1'] ?? ''));
}
$ok = inboxnest_verify(file_get_contents('php://input'), $_SERVER['HTTP_INBOXNEST_SIGNATURE'] ?? '', getenv('INBOXNEST_WEBHOOK_SECRET'));
Retries and turning off
Answer with any 2xx status within 10 seconds. Anything else (another status, a timeout, a redirect) is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. After that the delivery is marked failed. Handle events quickly and do slow work in the background.
After 20 failed deliveries in a row, InboxNest turns the endpoint off and emails the account owner. Fix it, turn it back on in Settings → Webhooks, and use Resend in its delivery log for anything you missed. The log keeps the last 100 attempts per endpoint, with the event, status code, time and attempt number.
Testing
Send test event in Settings → Webhooks (or POST /webhooks/{id}/test) sends a webhook.test event straight away and shows the result. A test is one attempt and isn’t retried.