Skip to content

Receive webhooks

Instead of polling GET /emails/{id}, let Vaartix tell you when something happens. Each event becomes a signed POST request to your server, usually within a few seconds.

  1. Add an endpoint to your app that accepts POST requests, for example https://api.acme.in/webhooks/vaartix. It must be public and use HTTPS.

  2. Register it with the events you want:

    Terminal window
    curl -X POST https://api.vaartix.com/webhooks \
    -H "Authorization: Bearer $VAARTIX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://api.acme.in/webhooks/vaartix",
    "events": ["email.delivered", "email.bounced", "email.complained"]
    }'

    The response contains a secret starting with whsec_. It’s shown only once: store it in your secrets manager or environment, for example as VAARTIX_WEBHOOK_SECRET.

  3. Verify every request with that secret before trusting it. See Verify signatures.

  4. Answer quickly with any 2xx status, within 10 seconds. Do slow work (sending a Slack message, updating a CRM) after responding, or in a background job.

With the Node.js SDK:

const { data, error } = await vaartix.webhooks.create({
url: 'https://api.acme.in/webhooks/vaartix',
events: ['email.delivered', 'email.bounced', 'email.complained'],
});
// data.secret: store it now, it isn't shown again.
Event When
email.sent Vaartix handed the email to the mail servers.
email.delivered The recipient’s mail server accepted it.
email.delivery_delayed Delivery is temporarily failing (for example a full mailbox) and is being retried.
email.bounced The address doesn’t exist or permanently refused it. Hard bounces are suppressed.
email.complained The recipient marked it as spam. The address is suppressed for your team.
email.rejected The email was refused before sending, for example because of a virus in an attachment.
email.rendering_failure A template couldn’t be rendered.
email.suppressed A recipient was skipped because it’s on a suppression list.
email.opened The email was opened, with open tracking on.
email.clicked A link in the email was clicked, with click tracking on.

Events for an email with several recipients come once per recipient, with data.recipient set. Emails to test addresses produce events too, so you can build and test your endpoint before going live.

POST /webhooks/vaartix HTTP/1.1
Content-Type: application/json
User-Agent: Vaartix-Webhooks/1.0
webhook-id: whd_8m3k1q7v2c9x4d6p
webhook-timestamp: 1793181600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "email.bounced",
"createdAt": "2026-10-28T10:00:02.000Z",
"data": {
"emailId": "em_7hq2kv9xr4m1c8d3",
"eventId": "evt_2m9k4q7v1c8x3d6p",
"recipient": "[email protected]",
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"cc": [],
"bcc": [],
"subject": "Your login code",
"tags": [{ "name": "flow", "value": "login" }],
"emailCreatedAt": "2026-10-28T10:00:00.000Z",
"details": {
"bounceType": "Permanent",
"bounceSubType": "General",
"diagnosticCode": "smtp; 550 5.1.1 user unknown",
"statusCode": "5.1.1",
"diagnostics": ["smtp; 550 5.1.1 user unknown"]
}
}
}

details carries what the mail provider reported, such as the bounce type or SMTP response. For email.bounced and email.delivery_delayed it includes the receiving server’s reply for that recipient, word for word, in diagnosticCode, and its status code in statusCode. A delay also has delayType and expirationTime (when retrying stops):

details of an email.delivery_delayed event
{
"delayType": "TransientCommunicationFailure",
"expirationTime": "2026-11-15T10:00:00.000Z",
"diagnosticCode": "smtp; 421 4.7.0 [TSS04] Messages from 54.240.0.1 temporarily deferred due to unexpected volume or user complaints - 4.16.55.1; see https://postmaster.yahooinc.com/error-codes",
"statusCode": "4.7.0"
}

See the receiving server’s reply for every field. The full schema is in the API reference.

email.opened and email.clicked come only for emails sent with tracking on (the domain’s setting, or tracking on the email), and only to endpoints that subscribe to them. Existing endpoints don’t get them until you add the events. Each open or click is one event; repeats are sent too.

{
"type": "email.clicked",
"createdAt": "2026-10-28T10:05:41.000Z",
"data": {
"emailId": "em_7hq2kv9xr4m1c8d3",
"eventId": "evt_5k2m8q1v7c3x9d4p",
"recipient": "[email protected]",
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"cc": [],
"bcc": [],
"subject": "Your order has shipped",
"tags": [{ "name": "flow", "value": "shipping" }],
"emailCreatedAt": "2026-10-28T10:00:00.000Z",
"details": {
"machine": false,
"machineReason": null,
"linkIndex": 0,
"link": "https://acme.in/orders/123",
"userAgentFamily": "Chrome",
"country": "IN",
"asn": 24560
}
}
}

email.opened has the same details without linkIndex and link.

Field
machine true when it was most likely not a person. Count only false ones as people.
machineReason Opens: image_proxy (Gmail, Yahoo), apple_mpp (Apple Mail Privacy Protection), too_fast (within 2 seconds of sending). Clicks: scanner, scanner_network, too_fast (within 10 seconds of delivery), burst (different links within 1 second). null when machine is false.
linkIndex, link Clicks only: the link’s position among the tracked links (from 0) and its original URL.
userAgentFamily A name such as Chrome, Apple Mail, Outlook or Gmail image proxy; other or unknown otherwise. The full user agent isn’t kept.
country, asn Where the request came from: a two-letter country code and network number, or null. No IP address is kept.

recipient is null when the email had more than one recipient, because an open or click can’t be tied to one of them. A click marked burst can be marked machine only after its webhook was sent, when the next link of the burst comes in; GET /emails/:id shows the final value.

If your server doesn’t answer with a 2xx within 10 seconds, Vaartix tries again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 6 hours: eight attempts over about a day. Redirects count as failures, so register the final URL.

If 5 deliveries in a row fail every attempt, the endpoint is turned off and its pending deliveries are cancelled. Fix the problem, then turn it back on:

Terminal window
curl -X PATCH https://api.vaartix.com/webhooks/wh_4n8x2k7q1m9c3v5d \
-H "Authorization: Bearer $VAARTIX_API_KEY" -H "Content-Type: application/json" \
-d '{ "status": "enabled" }'

GET /webhooks/{id}/deliveries lists recent deliveries with their status, number of attempts and the first 2 KB of your server’s response. To send one again, call POST /webhooks/{id}/deliveries/{deliveryId}/retry; it keeps the same webhook-id.

  • Ignore duplicates. The same delivery can arrive more than once (for example if your server timed out after processing it). Store each webhook-id you handle and skip ones you’ve seen.
  • Don’t rely on order. Deliveries can arrive out of order. Compare createdAt if order matters, and never move an email’s status backwards in your own database.
  • Respond first, work later. Queue the event and return 200 straight away.
  • Rotate secrets with POST /webhooks/{id}/rotate-secret if one may have leaked. The old secret stops working immediately, so update your server first.