Skip to article
InboxNest
← InboxNest home

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.