Polling an inbox on a timer works, but it means every receive is either too slow (long poll interval, mail sits unseen) or wasteful (short poll interval, mostly empty responses). A webhook sidesteps that entirely: set a webhook_url once, and DimeBox pushes a signed POST to it the instant a new message arrives, through either receive channel.
Pass webhook_url as an optional field on either provisioning route — the free POST /api/inboxes or the paid POST /api/inboxes/claim — alongside the rest of the request body, or set it afterward through the admin panel. The provisioning response then includes a webhook_secret, shown exactly once, right next to the read_token:
{
"data": { "slug": "acme-support-agent", ... },
"read_token": "inbox_...",
"webhook_secret": "whsec_...",
"paid_by": null,
"notice": "Save the read token (and webhook secret, if set) now - neither is shown again."
}
Save that secret with the same care you'd give the read token — see read tokens explained for why neither credential can be recovered after the fact if you lose it.
Today, exactly one event fires: message.received, triggered whenever either receive channel — the free third-party mail relay or the paid priority route — delivers a new message into the inbox. Outbound status changes (a queued send flipping to sent or failed) don't currently push a webhook; you still poll GET /api/inboxes/{slug}/messages for those. Polling vs webhooks covers that split in more depth, and how AI agents receive email covers the two receive channels this webhook is notifying you about.
Every webhook request carries a header, X-DimeBox-Signature: sha256=<hmac>, computed as an HMAC-SHA256 over the exact raw request body using the webhook_secret from setup. Verify it before trusting anything in the payload — don't parse the body first and check the signature after. A generic verification step looks like this:
// Node
const crypto = require('crypto');
function isValidSignature(rawBody, header, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(header)
);
}
# Python
import hashlib, hmac
def is_valid_signature(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
The constant-time comparison matters — a naive == string comparison on a signature check leaks timing information an attacker could use to forge a valid signature byte by byte. Both standard libraries above provide the constant-time primitive for exactly this reason; use it rather than rolling your own comparison.
The webhook payload is a notification, not necessarily the full message — treat it as a signal to go fetch the message rather than the message itself. Use the read token you saved at provisioning to pull the full content:
curl https://dimebox.app/api/inboxes/acme-support-agent/messages/ \
-H 'X-Inbox-Token: inbox_...'
From there, treat the content exactly like any other untrusted input before feeding it to an LLM or acting on it — see why you should treat inbound email as untrusted input for the reasoning.
A webhook tells you something arrived; it doesn't give you a longer memory of the inbox than the inbox itself has. The underlying message list is still capped and pruned the same way regardless of whether you're polling or getting pushed to — see the rolling message window for how that cap behaves. If your agent needs durable history beyond what the inbox retains, the webhook is the trigger to persist a message to your own storage the moment it arrives, not a substitute for doing so.
For the provisioning call where webhook_url gets set in the first place, see how to provision an inbox in one API call.
Treat your webhook handler the way you'd treat any endpoint receiving third-party input: verify the signature before parsing the body, respond quickly, and do the actual work — fetching the full message, running agent logic — after acknowledging receipt rather than inside the request itself. If your endpoint is slow to respond, that's a problem on your side of the integration to fix with a queue, not something to work around on DimeBox's end.
Because the webhook is just a signed POST to whatever URL you configured, testing it doesn't require anything DimeBox-specific — point webhook_url at a tunnel to your local machine while developing, confirm the signature verifies against the webhook_secret you were given at setup, and trigger a real message through either receive channel to see the full round trip. There's no separate sandbox-only webhook mode to configure; the same mechanism fires in testing and in production, which is also why testing on Base Sepolia is worth doing in parallel if the messages triggering your webhook are themselves paid through the priority channel.