DMARCbis-ready · The 2026 standard

Receiving webhook events for critical issues and DNS fixes

Outbound webhooks push events to you instead of making you poll. Register an HTTPS endpoint, and trustyourinbox POSTs a signed JSON payload the instant a critical issue is detected or a one-click DNS fix lands, so your own systems can page on-call, open a ticket, or post to Slack in real time. Here's how to add an endpoint, verify that a delivery really came from us, what the payload looks like, and how retries and de-duplication work.

What outbound webhooks give you

A webhook is a small HTTP request we send to a URL you own, the moment something worth reacting to happens. Instead of checking the dashboard or calling the API on a timer, your systems find out immediately and can act: page the on-call engineer, open a ticket, drop a message in a Slack channel, or kick off a runbook.

We send an event when:

  • a critical issue is detected on one of your domains (a crit-severity anomaly, like alignment collapsing or a DKIM key going missing),
  • a one-click DNS fix is applied to your DNS,
  • a DNS fix is reverted (the 24-hour undo, or a manual rollback),
  • and a DNS fix fails to apply, so you know a change you expected didn't land.

Outbound webhooks are a Pro plan feature and up. You manage them in the dashboard under Settings, then Integrations, then Webhooks.

Step 1: Add an endpoint

You'll need a URL that can receive an HTTP POST. It must be HTTPS and resolve to a public address. In Settings, then Integrations, then Webhooks, click Add endpoint, paste the URL, and save.

We show you a signing secret once, at creation. Copy it now and store it like a password. It's what lets your code prove a request genuinely came from us (Step 2). If you lose it, rotate the endpoint to get a new one. Each endpoint has its own secret.

Every delivery we send carries three headers your handler should read:

  • X-TYI-Signature - the HMAC signature, formatted as sha256=<hex>.
  • X-TYI-Timestamp - the unix time (in seconds) the delivery was signed.
  • X-TYI-Event-Id - a stable id for this event, used for de-duplication.

Step 2: Verify the signature

Anyone who learns your URL could POST to it, so never trust a payload until you've checked the signature. We sign each delivery by computing an HMAC-SHA256 over the string {timestamp}.{rawBody} (the X-TYI-Timestamp value, a literal dot, then the exact request body) using your endpoint's signing secret. You recompute the same value and compare it, in constant time, to the X-TYI-Signature header.

The one thing to get right: hash the raw request body, the exact bytes we sent, before any framework parses or re-serializes the JSON. Re-serializing changes whitespace and key order, and the signature won't match. Here's the check in Node:

import crypto from "node:crypto";

// The signing secret shown once when you created the endpoint.
const SIGNING_SECRET = process.env.TYI_WEBHOOK_SECRET;

// rawBody MUST be the exact bytes we sent, before any JSON parse or
// re-serialize. Read it as a raw string/buffer, not the parsed object.
function isFromTrustYourInbox(headers, rawBody) {
  const signature = headers["x-tyi-signature"]; // "sha256=<hex>"
  const timestamp = headers["x-tyi-timestamp"]; // unix seconds

  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", SIGNING_SECRET)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

  // Constant-time compare so a mismatch can't be timed.
  const a = Buffer.from(signature || "");
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

If the comparison fails, respond 401 and drop the request. As a second layer, you can reject deliveries whose X-TYI-Timestamp is more than a few minutes old to blunt replay attempts.

The payload

Every delivery is a JSON object at version 1. Reply 2xx quickly to acknowledge it; if you need to do slow work, enqueue it and return right away. A crit anomaly looks like this:

{
  "version": 1,
  "event": "anomaly.crit",
  "id": "evt_9f2a1c7b4e3d",
  "occurredAt": "2026-07-09T14:03:22Z",
  "workspaceId": "ws_3a8f21",
  "data": {
    "domainId": "dom_5c1e90",
    "domainName": "acme.com",
    "severity": "crit",
    "anomalyType": "low_alignment",
    "title": "Alignment dropped below 85% for acme.com",
    "body": "Aligned mail fell to 78% over the last 24 hours. A sending source may have broken authentication.",
    "url": "https://app.trustyourinbox.com/dashboard/domains/dom_5c1e90"
  }
}

The top-level fields are stable across every event type:

  • version - the payload schema version. It's 1 today; we'll bump it if the shape ever changes so your handler can branch on it.
  • event - which of the four event types this is (below).
  • id - the event id, the same value as the X-TYI-Event-Id header. Use it to de-duplicate.
  • occurredAt - an ISO 8601 timestamp for when the event happened.
  • workspaceId - the workspace the event belongs to.
  • data - the event detail: domainId, domainName, severity, an optional anomalyType (present on anomaly.crit), a plain-English title and body, and a url that deep-links to the relevant page in the app.

The events we send

  • anomaly.crit - a critical issue was detected on a domain. data.anomalyType names which check fired.
  • dns_fix.applied - a one-click DNS fix was written to your DNS.
  • dns_fix.reverted - a DNS fix was rolled back, either through the 24-hour undo or manually.
  • dns_fix.failed - a DNS fix we attempted did not apply.

The three dns_fix.* events are the tail end of the same safety pipeline every one-click fix runs through. If you want the full picture of what happens between the button and your DNS, see how we update DNS records safely.

Delivery, retries, and auto-disable

Delivery is at-least-once. If your endpoint is slow, errors, or returns a non-2xx status, we retry with exponential backoff, up to four attempts. Because a delivery can arrive more than once (a retry after your server already processed it but the acknowledgement was lost), your handler must be idempotent: de-duplicate on X-TYI-Event-Id. That id is stable for the same event for 14 days, which is comfortably longer than the retry window, so recording the ids you've already handled and skipping repeats is all it takes.

A few delivery rules worth knowing. We don't follow redirects (a 3xx counts as a failure, so point us straight at the final URL), and we only connect to public addresses. Endpoints that resolve to private or internal ranges are refused, so a webhook can't be pointed at something inside our own network.

If an endpoint keeps failing, we stop wasting deliveries on it. After 20 consecutive failures, the endpoint is automatically disabled. Fix the receiver, then re-enable it in Settings, then Integrations, then Webhooks. Nothing is deleted; it just stops sending until you turn it back on.

Testing and the delivery log

Every endpoint has a Send test button that delivers a sample event, so you can wire up and verify your handler before a real one ever fires. Each endpoint also keeps a delivery log: what we sent, the response we got back, and whether it succeeded or is being retried. If a real event doesn't show up where you expect, the log is the first place to look, it tells you whether we delivered it and what your server said.

Doing it from the API or an AI assistant

Everything above is also available programmatically. The REST API manages endpoints (POST /v1/webhooks to create one, plus the usual list, read, and delete), and there are MCP tools like create_webhook_endpoint so an AI assistant can set one up on your behalf. Both are documented in the API reference.

Troubleshooting

Every delivery fails the signature check. Almost always this is the raw-body problem: you're hashing a re-serialized version of the JSON instead of the exact bytes we sent. Capture the raw body before your framework parses it, and confirm you're signing {timestamp}.{rawBody} with a literal dot between them, using the right endpoint's secret.

The endpoint went quiet. Check whether it was auto-disabled after 20 consecutive failures. Open its delivery log to see what your server returned, fix that, then re-enable the endpoint.

We won't accept my URL. It has to be HTTPS and resolve to a public address. A http:// URL, a private or internal host, or one that only answers behind a redirect will be refused.

I'm getting the same event twice. That's expected under at-least-once delivery. De-duplicate on X-TYI-Event-Id, which stays stable for 14 days per event.

Still stuck? Open the help bubble in the dashboard and send us a note. We read every one.

Keep reading

Stop guessing. Start monitoring.

Free for one domain. Set up in five minutes. We parse the reports; you read plain-English summaries.