Get notified when something happens
Subscribe an endpoint to email events, and verify that a delivery really came from us.
Webhooks are how your application receives notifications when email events occur — deliveries, opens, clicks, bounces. NevarMail publishes the email.* events below as your provider reports them; the contact.* and domain.* events are not dispatched yet (see the callout under Event types).
Overview
NevarMail supports both inbound routing (processing emails your account receives) and outbound event webhooks (notifications about email events, delivered to endpoints you register).
For inbound email processing, see Inbound Routing.
Event webhooks
Register one or more webhook endpoints and NevarMail will fan out matching events to each of them in real time. This also lets you work around a provider's own webhook-count limit: register as many NevarMail endpoints as you need and NevarMail delivers to all of them from a single upstream event.
All endpoints below use the standard v1 API envelope ({ data, requestId, pagination? } on success) and require the webhooks:read scope for reads and webhooks:write for writes.
Base URL: https://app.nevarmail.com (local dev: http://localhost:3400)
Create an endpoint
POST /api/v1/webhook-endpoints{
"url": "https://your-app.com/webhooks/nevarmail",
"description": "Production event handler",
"events": ["email.delivered", "email.bounced"]
}events accepts any of the event types below, or ["*"] to subscribe to all events. Omitting events (or passing []) also matches every event.
Response (201):
{
"data": {
"id": "...",
"url": "https://your-app.com/webhooks/nevarmail",
"description": "Production event handler",
"events": ["email.delivered", "email.bounced"],
"isActive": true,
"failureCount": 0,
"maxFailures": 5,
"lastSuccessAt": null,
"lastFailureAt": null,
"createdAt": "2026-03-22T12:00:00.000Z",
"updatedAt": "2026-03-22T12:00:00.000Z",
"secret": "whsec_..."
},
"requestId": "..."
}maxFailures defaults to 5. When failureCount reaches it after 5 consecutive failed deliveries, NevarMail automatically disables the endpoint (isActive becomes false) and stops sending to it. Re-enable it with PATCH /api/v1/webhook-endpoints/:id and {"isActive": true}, which also resets failureCount back to 0.
The secret is returned only in this response. Store it immediately — NevarMail cannot show it to you again (rotating it via the endpoint-secret route issues a new one, which also can't be retrieved later).
The target url must be a public HTTPS address; NevarMail rejects private/loopback hosts.
List endpoints
GET /api/v1/webhook-endpoints?page=1&perPage=25Returns the same shape as above, without secret, wrapped in the envelope's pagination field (page, perPage, total).
Get an endpoint
GET /api/v1/webhook-endpoints/:idUpdate an endpoint
PATCH /api/v1/webhook-endpoints/:id{
"events": ["email.delivered", "email.bounced", "email.dropped"],
"isActive": true
}Any of url, description, events, isActive may be included; omitted fields are left unchanged.
Delete an endpoint
DELETE /api/v1/webhook-endpoints/:idEvent types
contact.* and domain.* are not dispatched yet
Every email.* event below is delivered from real sending activity.
contact.suppressed, domain.verified and domain.failed are valid
subscription values, but nothing dispatches them yet: an endpoint subscribed
only to them receives nothing. Use the suppression list and the domain status
in the meantime.
The events you can subscribe to:
| Event | Description |
|---|---|
email.sent | Email was accepted by the provider |
email.delivered | Email was delivered to the recipient |
email.deferred | Delivery was temporarily delayed by the receiving server |
email.dropped | The provider dropped the email before attempting delivery |
email.bounced | Email bounced |
email.opened | Recipient opened the email |
email.clicked | Recipient clicked a link |
email.complained | Recipient marked the email as spam |
email.unsubscribed | Recipient unsubscribed |
contact.suppressed | A recipient was added to your suppression list |
domain.verified | A sending domain passed verification |
domain.failed | A sending domain failed verification |
Payload format
The HTTP body NevarMail POSTs to your endpoint is the raw JSON payload for the event — there is no extra { event, timestamp, data } wrapper around it.
Every email.* event has this shape:
{
"type": "email.clicked",
"occurred_at": "2026-03-22T12:00:03.000Z",
"project_id": "6f1c2a9e-0d4b-4f3e-9a51-2b7c8d9e0f12",
"email_id": "0b9a7c2e-5d1f-4e8a-b3c6-7f2d1e0a9b84",
"campaign_send_id": null,
"recipient": "someone@example.com",
"send_mode": "production",
"url": "https://example.com/pricing"
}| Field | Description |
|---|---|
type | The event type, matching the subscription value |
occurred_at | When the provider reports the event happened (ISO 8601) |
project_id | The project the message was sent from |
email_id | The id the send API returned for the message, or null for a campaign send |
campaign_send_id | The campaign send the message belongs to, or null |
recipient | The recipient address the event is about |
send_mode | production or test — events from test-mode sends are delivered too, so filter on this if you only want live mail |
url | email.clicked only: the link that was clicked |
reason | The provider's reason, when it gives one (typically on email.bounced and email.dropped) |
An event goes to every active endpoint subscribed to its type, both org-wide endpoints and endpoints scoped to the message's project. Your provider may report the same event more than once; NevarMail publishes each provider event once.
Test events
An endpoint's Send test event action in the dashboard (POST /api/webhook-endpoints/:id/test) sends email.sent with this body:
{
"test": true,
"endpoint_id": "550e8400-e29b-41d4-a716-446655440000",
"message": "This is a test webhook event from NevarMail",
"timestamp": "2026-03-22T12:00:03.000Z"
}Verifying signatures
Every delivery is signed with HMAC-SHA256, following the Standard Webhooks (Svix) convention. Each request carries:
| Header | Description |
|---|---|
X-Webhook-Id | Unique ID for this delivery attempt |
X-Webhook-Timestamp | Unix timestamp (seconds) the request was signed |
X-Webhook-Signature | v1,<signature> — a base64-encoded HMAC-SHA256 digest |
The signature is computed over the exact string {id}.{timestamp}.{body}, where {id} is the value of X-Webhook-Id, {timestamp} is the value of X-Webhook-Timestamp, and {body} is the raw, unparsed request body — using your endpoint's secret (the whsec_... prefix is stripped before signing) as the HMAC key.
To verify a request:
- Read the raw request body before any JSON parsing — signature verification must run against the exact bytes NevarMail sent.
- Reject the request if
X-Webhook-Timestampis more than 5 minutes old or from the future, to guard against replay. - Recompute the signature and compare it to
X-Webhook-Signatureusing a constant-time comparison.
const crypto = require("crypto");
function verifyNevarMailWebhook(webhookId, timestamp, rawBody, secret, signatureHeader) {
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) {
throw new Error("Timestamp outside tolerance");
}
const rawSecret = secret.startsWith("whsec_") ? secret.slice(6) : secret;
const signedContent = `${webhookId}.${timestamp}.${rawBody}`;
const expected =
"v1," +
crypto.createHmac("sha256", rawSecret).update(signedContent).digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("Invalid signature");
}
}
// Express example (requires the raw body, not the parsed one):
app.post(
"/webhooks/nevarmail",
express.raw({ type: "application/json" }),
(req, res) => {
verifyNevarMailWebhook(
req.header("X-Webhook-Id"),
req.header("X-Webhook-Timestamp"),
req.body, // Buffer — do not JSON.parse before verifying
process.env.NEVARMAIL_WEBHOOK_SECRET,
req.header("X-Webhook-Signature"),
);
res.sendStatus(200);
},
);Retries and delivery history
Failed deliveries are tracked per endpoint (failureCount / lastFailureAt on the endpoint). A failed delivery (a non-2xx response, timeout, or network error) is retried up to 5 times with exponential backoff, starting at roughly a 10-second delay and increasing with jitter between attempts — 6 total attempts before a delivery is marked failed for good. As described above, 5 consecutive failed deliveries disable the endpoint automatically.
View delivery history, including response codes and errors, for an endpoint from its detail page in the dashboard. There is currently no /api/v1 endpoint for delivery history — it's only available in the dashboard UI, not via API key.
Need webhooks now?
While you build out your webhook handler, you can:
- Poll the analytics API -- With an API key,
GET /api/v1/analytics/summarygives org-level counts andGET /api/v1/analytics/messages/:messageIdgives one message's event timeline. The olderGET /api/analytics/stats/:emailIdis browser-session only — an API key gets403 SESSION_ONLY. - Use inbound routing -- Configure inbound routing rules to forward emails to webhook URLs
- Track events manually --
POST /api/analytics/trackrecords events from your own provider webhook handler; it is also session-only, so it suits an internal tool, not a server-to-server integration