Documentation For agents

Event webhooks

MailGlo sends a signed message to your HTTPS endpoint when something happens in your account, such as a new case, a change result or a reporting delay. Your ticketing system, chat or script can then act on it right away. Webhooks are on every plan.

Add an endpoint

  1. Open Settings, then Webhooks, and enter your endpoint's HTTPS address.
  2. Copy the signing secret. It is shown once. Keep it with your endpoint's other secrets.
  3. Choose Send test. Your endpoint proves it is yours by returning the test's challenge value in its response body. Events start once it is verified.

Scripts and agents use the same steps through the API: POST /api/v1/msps/<organization-id>/webhooks with {"url": "https://..."}, then POST .../webhooks/<id>/test.

What you receive

Each delivery is a POST with a JSON body, the same event the events API (GET /api/v1/msps/<organization-id>/events) returns:

{"id": 4211, "kind": "operation.outcome", "version": 1, "msp_id": "...", "client_id": "...", "domain_id": "...",
 "resource_kind": "change_plan", "resource_id": "...", "correlation": "...", "result": "succeeded", "at": "2026-10-30T09:12:44Z"}

An event names what changed and links to it by ID. Read the details from the API with the same permissions. These kinds are sent:

  1. case.opened: a case was opened, such as a new sender or an authentication failure to look into.
  2. case.updated, case.corrected and case.result: a case changed, was corrected or got its result.
  3. approval.decided: a DNS change was approved, declined or sent back for more information.
  4. operation.outcome: a DNS change ran; result says whether it was applied, is waiting, failed or is unknown.
  5. sender.decided: a sender was confirmed or rejected.
  6. credential.revoked and credential.rotated: an API token or agent was revoked, or an agent's token was replaced.
  7. data.delayed: reports for a domain stopped arriving on time.
  8. billing.state and usage.warning: a renewal payment failed, or reported email stayed above the plan's allowance for several months.
  9. dns.record_changed: MailGlo's scheduled DNS check found that a domain's published record changed since the previous check. One event is sent for each changed record, with the change in data.

DNS record changes

A dns.record_changed event carries the change itself in data, so your endpoint does not need to read it back:

{"id": 4310, "kind": "dns.record_changed", "version": 1, "msp_id": "...", "client_id": "...", "domain_id": "...",
 "resource_kind": "dns_record_change", "resource_id": "...", "correlation": "dns_record_change:...", "result": "changed", "at": "2026-10-30T10:00:05Z",
 "data": {"domain": "example.com", "record": "dmarc", "label": "DMARC", "name": "_dmarc.example.com", "type": "TXT",
          "before": ["v=DMARC1; p=none; rua=mailto:dmarc@example.com"], "after": ["v=DMARC1; p=reject; rua=mailto:dmarc@example.com"],
          "ownership": "customer_managed", "previous_ownership": "customer_managed",
          "observed_at": "2026-10-30T10:00:02Z", "snapshot_id": "...", "previous_snapshot_id": "..."}}
  1. record is the record compared: dmarc, spf, mx, mta_sts, tls_rpt, bimi or dkim:<selector>. name and type are its DNS name and type.
  2. before and after list the published values, sorted. An empty list means no record was published.
  3. ownership says who publishes the record now: customer_managed for your own DNS, mailglo for a record MailGlo hosts for you, or unknown. previous_ownership says the same for the previous check. The apex SPF record is always customer_managed, even when it includes MailGlo's hosted SPF.
  4. observed_at is when the check read the new values. resource_id names the recorded change; one check that finds several records changed sends one event for each, with the same resource_id.

An endpoint receives a domain's changes only if its person or agent may read that domain's client.

A test delivery has the kind webhook.test and a challenge field.

Who receives what

An endpoint receives only the events the person or agent who added it may read, checked again before every delivery. An agent limited to one client receives that client's events and account-wide events, never another client's. If the person or agent loses access, the endpoint is turned off. While an owner pauses agent access, deliveries to an agent's endpoints wait until it resumes.

Owners and people who manage agents see and manage every endpoint in Settings; everyone else sees their own.

Verify the signature

Every delivery carries these headers (header names are case-insensitive):

  1. MailGlo-Signature: t=<unix seconds>,v1=<signature>: the signature is the hex HMAC-SHA256 of <t>.<raw body>, keyed with your signing secret, the whole whsec_... string.
  2. MailGlo-Event: the event kind.
  3. MailGlo-Delivery: the delivery ID, the same on every retry. Use it to ignore a delivery you already handled.
  4. MailGlo-Attempt: 1 for the first try.

Check the signature against the raw body before parsing it, and refuse a timestamp more than 5 minutes from your clock, so a recorded delivery cannot be replayed later. In Node.js:

import crypto from "node:crypto";

export function verify(secret, header, rawBody, now = Date.now()) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(now / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return parts.v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

In Python:

import hashlib, hmac, time

def verify(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Answer the test delivery with its challenge:

if (event.kind === "webhook.test") return res.status(200).send(event.challenge);

Retries and the delivery log

Answer with any 2xx status within 10 seconds. Any other answer, a timeout or a connection error counts as a failed attempt. An event is tried up to 8 times in all: the first try, then retries after 1, 4, 16 and 64 minutes and then every 4 hours. After the eighth failed attempt the delivery is marked failed. A test delivery is tried once; send another test to try again. Deliveries can arrive more than once or out of order; use MailGlo-Delivery and the event id to handle each event once.

MailGlo keeps a 30-day log for each endpoint: the event, the result, the number of attempts, your endpoint's response code and the last problem. Settings, Webhooks shows the latest 50 entries, or up to 200; GET /api/v1/msps/<organization-id>/webhooks/<id>/deliveries?limit=200 returns the same.

An endpoint whose deliveries have all failed for 24 hours is turned off, and the account's owners get an email. Fix the endpoint, then choose Turn on. Events from while it was off are kept in the events API for 30 days; read them there.

Limits and safety

  1. Endpoints use HTTPS on a public address. MailGlo connects only to public addresses, checked on every connection. A redirect counts as a failed delivery.
  2. An account can have up to 10 endpoints.
  3. Every attempt counts toward a limit of 2,000 per account in any 24 hours: first tries, retries and tests alike. When the limit is reached, events wait and are tried again as the oldest attempts leave the 24-hour window, and a test answers budget_exhausted (HTTP 429) at once. An event that is still not delivered 3 days after it happened is marked failed. Nothing is charged.
  4. MailGlo reads up to 64 KB of your endpoint's response.

Documentation version docs-2026-10-12.

Found an error, or a question this page does not answer? Email support@mailglo.com.