Overview

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.

TopicSent when
email.sentA transactional email is sent.
email.openedA recipient opens a transactional email.
email.clickedA recipient clicks a link in a transactional email.
email.bouncedA transactional email bounces.
email.complaintA recipient marks a transactional email as spam.
domain.verifiedOne of your sending domains passes verification.
domain.unverifiedOne 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:

HeaderValue
Content-Typeapplication/json; charset=utf-8
X-DirectIQ-TopicThe event's topic, for example email.opened.
X-DirectIQ-SignatureThe 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"
}
FieldDescription
client_idYour DirectIQ account ID.
transactional_idThe ID of the transactional email, the same ID /v2/transactionals/{id} uses.
bodyThe full HTML of the email, so payloads can be large.
created_utcWhen 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.

  1. Read the raw body before parsing it as JSON. Parsing and re-serializing changes the bytes, and the signature won't match.
  2. Compute the HMAC-SHA256 of the raw body with your secret.
  3. Compare the result with the X-DirectIQ-Signature header using a constant-time comparison.
  4. If they don't match, respond with 401 and 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 "", 200
app.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:

AttemptWhen
1Right away
230 seconds after attempt 1 fails
31 minute after attempt 2 fails
45 minutes after attempt 3 fails
515 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.opened can arrive before its email.sent. Use created_utc to order events, not the time they arrive.

Best practices

  • Use an https:// URL on a public host name. DirectIQ rejects http:// 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 status to inactive.