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.
-
Add an endpoint to your app that accepts
POSTrequests, for examplehttps://api.acme.in/webhooks/vaartix. It must be public and use HTTPS. -
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
secretstarting withwhsec_. It’s shown only once: store it in your secrets manager or environment, for example asVAARTIX_WEBHOOK_SECRET. -
Verify every request with that secret before trusting it. See Verify signatures.
-
Answer quickly with any
2xxstatus, 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.Events
Section titled “Events”| 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.
The request
Section titled “The request”POST /webhooks/vaartix HTTP/1.1Content-Type: application/jsonUser-Agent: Vaartix-Webhooks/1.0webhook-id: whd_8m3k1q7v2c9x4d6pwebhook-timestamp: 1793181600webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={ "type": "email.bounced", "createdAt": "2026-10-28T10:00:02.000Z", "data": { "emailId": "em_7hq2kv9xr4m1c8d3", "eventId": "evt_2m9k4q7v1c8x3d6p", "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):
{ "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.
Opens and clicks
Section titled “Opens and clicks”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", "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.
Retries
Section titled “Retries”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:
curl -X PATCH https://api.vaartix.com/webhooks/wh_4n8x2k7q1m9c3v5d \ -H "Authorization: Bearer $VAARTIX_API_KEY" -H "Content-Type: application/json" \ -d '{ "status": "enabled" }'Delivery log
Section titled “Delivery log”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.
Good practice
Section titled “Good practice”- Ignore duplicates. The same delivery can arrive more than once (for example if your server
timed out after processing it). Store each
webhook-idyou handle and skip ones you’ve seen. - Don’t rely on order. Deliveries can arrive out of order. Compare
createdAtif order matters, and never move an email’s status backwards in your own database. - Respond first, work later. Queue the event and return
200straight away. - Rotate secrets with
POST /webhooks/{id}/rotate-secretif one may have leaked. The old secret stops working immediately, so update your server first.