Get notified in real time when events happen in your DirectIQ account.
Webhooks
Webhooks notify your server when something happens in your DirectIQ account, such as a transactional email being opened or a domain being verified. DirectIQ sends an HTTP POST request to a URL you choose each time an event happens, so you don't need to poll the API.
curl https://api.directiq.com/v2/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/directiq/webhooks",
"topics": ["email.opened", "email.clicked"]
}'The response includes the webhook's secret. It's shown only once, so store it somewhere safe. You need it to verify requests. If you send your own secret when you create the webhook, DirectIQ uses that one.
You can also manage webhooks in DirectIQ under Integrations → Webhooks.
Topics
Each webhook subscribes to one or more topics. Set "all_topics": true to receive every topic, including ones added later.
| Topic | Sent when |
|---|---|
email.sent | A transactional email is sent. |
email.opened | A recipient opens a transactional email. |
email.clicked | A recipient clicks a link in a transactional email. |
email.bounced | A transactional email bounces. |
email.complaint | A recipient marks a transactional email as spam. |
domain.verified | One of your sending domains passes verification. |
domain.unverified | One of your sending domains fails verification. |
The email.* topics cover transactional emails only. Campaign sends, opens and clicks don't trigger webhooks.
To get the current list from the API, call GET /v2/webhooks/topics.
The request
DirectIQ sends each event as a POST request with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json; charset=utf-8 |
X-DirectIQ-Topic | The event's topic, for example email.opened. |
X-DirectIQ-Signature | The signature of the request body. See Verify the signature. |
The topic is sent only in the header, not in the body.
Payloads
email.sent, email.opened
{
"client_id": 12345,
"transactional_id": "8f14e45f-ceea-4e7a-9b1c-2a5d7f3e9c10",
"from_email": "[email protected]",
"to_email": "[email protected]",
"subject": "Your order has shipped",
"body": "<html>…</html>",
"created_utc": "2026-11-10T09:30:00Z"
}email.clicked
The same fields, plus the link that was clicked:
{
"client_id": 12345,
"transactional_id": "8f14e45f-ceea-4e7a-9b1c-2a5d7f3e9c10",
"from_email": "[email protected]",
"to_email": "[email protected]",
"subject": "Your order has shipped",
"body": "<html>…</html>",
"link": "https://yourcompany.com/orders/1001",
"created_utc": "2026-11-10T09:31:12Z"
}email.bounced, email.complaint
The same fields as email.sent, plus bounce_type for email.bounced or spam_source for email.complaint.
domain.verified, domain.unverified
{
"client_id": 12345,
"domain": "mail.yourcompany.com",
"created_utc": "2026-11-10T09:30:00Z"
}| Field | Description |
|---|---|
client_id | Your DirectIQ account ID. |
transactional_id | The ID of the transactional email, the same ID /v2/transactionals/{id} uses. |
body | The full HTML of the email, so payloads can be large. |
created_utc | When the event happened, in UTC. |
Fields with no value are left out.
Verify the signature
Every request is signed with your webhook's secret. Check the signature before you trust the request. Otherwise anyone who finds your URL can send you fake events.
The signature is an HMAC-SHA256 of the raw request body, keyed with your secret and hex-encoded in lowercase.
- Read the raw body before parsing it as JSON. Parsing and re-serializing changes the bytes, and the signature won't match.
- Compute the HMAC-SHA256 of the raw body with your secret.
- Compare the result with the
X-DirectIQ-Signatureheader using a constant-time comparison. - If they don't match, respond with
401and ignore the event.
const crypto = require("crypto");
const express = require("express");
const app = express();
app.post("/directiq/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.DIRECTIQ_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const received = req.get("X-DirectIQ-Signature") || "";
if (received.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.sendStatus(401);
}
const topic = req.get("X-DirectIQ-Topic");
const event = JSON.parse(req.body);
// queue the event for processing, then respond
res.sendStatus(200);
});import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["DIRECTIQ_WEBHOOK_SECRET"].encode()
@app.post("/directiq/webhooks")
def directiq_webhook():
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
received = request.headers.get("X-DirectIQ-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
topic = request.headers.get("X-DirectIQ-Topic")
event = request.get_json()
# queue the event for processing, then respond
return "", 200app.MapPost("/directiq/webhooks", async (HttpRequest request) =>
{
using var ms = new MemoryStream();
await request.Body.CopyToAsync(ms);
var body = ms.ToArray();
var secret = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("DIRECTIQ_WEBHOOK_SECRET")!);
var expected = Convert.ToHexString(HMACSHA256.HashData(secret, body)).ToLowerInvariant();
var received = request.Headers["X-DirectIQ-Signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)))
return Results.Unauthorized();
var topic = request.Headers["X-DirectIQ-Topic"].ToString();
// queue the event for processing, then respond
return Results.Ok();
});Respond quickly
Respond with any 2xx status within 10 seconds to confirm you received the event. DirectIQ ignores the response body.
If processing takes longer, save the event, respond right away and handle it in the background. A request that times out counts as failed, even if your server finished the work.
Retries
If your endpoint responds with a non-2xx status, times out or can't be reached, DirectIQ tries again. Each event is attempted up to 5 times:
| Attempt | When |
|---|---|
| 1 | Right away |
| 2 | 30 seconds after attempt 1 fails |
| 3 | 1 minute after attempt 2 fails |
| 4 | 5 minutes after attempt 3 fails |
| 5 | 15 minutes after attempt 4 fails |
If all 5 attempts fail, DirectIQ stops sending that event.
Duplicates and ordering
- You may get the same event more than once. For example, if your server processed an event but responded too late, DirectIQ sends it again. Make your handler safe to run twice for the same event.
- Events can arrive out of order. An
email.openedcan arrive before itsemail.sent. Usecreated_utcto order events, not the time they arrive.
Best practices
- Use an
https://URL on a public host name. DirectIQ rejectshttp://URLs, IP addresses and internal host names. - Keep your webhook secret on your server, like your API key. If it leaks, set a new one with
PATCH /v2/webhooks/{id}. - Subscribe only to the topics you need. Email events include the full email body.
- To pause deliveries without deleting the webhook, set its
statustoinactive.

