Relay webhooks
Receive delivery, bounce and complaint events for SMTP relay and Email API messages, and verify their signature.
Webhooks send an HTTP POST to your application whenever a relay or API message changes status, so you don't need to poll the API.
Set up
- Go to SMTP & API → Webhooks.
- Enter a public
https://URL on your server and click Save. Private and local addresses are not accepted. - Click Reveal to copy the signing secret, and Send test event to check that your endpoint responds.
Events
| Event | When |
|---|---|
relay.sent |
The message was handed to the delivery provider for this recipient |
relay.delivered |
The receiving server accepted the message (when the provider reports deliveries) |
relay.bounced |
The message bounced. Hard bounces are added to your suppression list |
relay.complained |
The recipient marked the message as spam. The address is suppressed |
relay.failed |
Delivery failed permanently. The credit is refunded |
relay.suppressed |
The recipient was skipped because of an earlier bounce or complaint (not charged) |
Example body:
{
"event": "relay.bounced",
"message_id": "01JABCDEF0123456789XYZ0000",
"smtp_message_id": "01JABCDEF0123456789XYZ0000@smtp.inboxroads.com",
"recipient": "jane@example.com",
"status": "bounced",
"subject": "Your order #1024",
"tags": ["order-confirmation"],
"bounce_type": "hard_bounce",
"diagnostic": "550 5.1.1 user unknown",
"timestamp": "2026-10-01T09:30:00+00:00"
}
Verify the signature
Every request has the header X-InboxRoads-Signature: t=<unix time>,v1=<signature>. The signature is HMAC-SHA256 of t + "." + raw request body, using your signing secret. Reject requests where the signature doesn't match or t is more than 5 minutes old.
PHP:
[$t, $v1] = array_map(fn ($p) => explode('=', $p, 2)[1], explode(',', $_SERVER['HTTP_X_INBOXROADS_SIGNATURE']));
$expected = hash_hmac('sha256', $t.'.'.file_get_contents('php://input'), $secret);
$valid = hash_equals($expected, $v1) && abs(time() - (int) $t) < 300;
Node.js:
const [t, v1] = header.split(',').map(p => p.split('=')[1]);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
Delivery and retries
Respond with any 2xx status within 10 seconds. If your endpoint returns a 5xx or 429, or can't be reached, we retry up to 5 more times over about 3 hours. Events may occasionally arrive more than once or out of order, so use message_id + recipient + event to de-duplicate.
Still stuck? Our team answers every message.
Contact supportLast updated September 30, 2026