Documentation 10 min read Updated June 13, 2026

How to send Azure App Registration expiration alerts to a JSON webhook

A client secret never fails loudly. Nothing in your code changed, nothing deployed, nobody touched the app — and then one morning a background job starts returning 401 because the credential quietly reached its expiry date. If you would rather catch that in your own systems than in an incident channel, Token Watch can POST a signed JSON alert to an endpoint you control, while the secret is still expiring rather than expired.

The frustrating part is not that Entra hides the expiration date — it is right there in the portal. The problem is that the date never travels to where your team actually reacts: a queue, a ticketing workflow, a SIEM, or that one small internal service that happens to know who owns each app.

A webhook is not magic; it is just plumbing to carry that signal from Token Watch into your own path. Reach for the JSON channel when the thing on the other end is software rather than a person — when you want to verify, route, deduplicate, and store the alert instead of just reading it. If a human glancing at a message is all you need, Slack or Teams will be a lot less work.

Why this matters

When an app-registration credential expires, it usually takes something useful down with it: a nightly job, a CI/CD deployment, a daemon app, a service-to-service call, an integration with some SaaS vendor. And the blast radius only grows as you add tenants, environments, and registrations — usually faster than anyone updates the spreadsheet that was meant to track them.

Calendar reminders and spreadsheets rot. The alert that actually earns its keep is the one that reaches the right people before the expiry, and carries enough metadata to route the work without a human triaging it first.

Integration overview

Microsoft Entra ID / Azure AD App Registrations
  -> Token Watch
  -> signed JSON webhook
  -> your receiver, queue, SIEM, or ticketing workflow

Once you grant admin consent, Token Watch syncs your App Registration credential metadata through Microsoft Graph. A daily monitoring run checks the tracked credentials, and when something is actually expiring or already expired, it sends an alert. On a day where nothing is wrong there is no webhook at all — an empty alert is never delivered, so a quiet endpoint means a quiet tenant.

The payload is metadata only: application name, secret or certificate display name, expiry timestamp, status, tenant ID, and report ID. Token Watch cannot read secret values in the first place — Graph does not hand them back — so it never sends client secret values, certificate private keys, user passwords, or payment details. There is nothing sensitive in the body to leak.

When to use a JSON webhook

Reach for the JSON channel when you need routing, ownership lookup, ticket creation, incident workflows, SIEM ingestion, dashboards, or deduplication — anything where another system has to make a decision. For a one- or two-person team, email may genuinely be enough. For visible triage in a shared channel, Slack or Teams will do. The JSON webhook earns its place when the receiver is code. If a readable notification is all you are after, the Slack guide and the Microsoft Teams guide cover those paths with far less wiring.

Prerequisites

  • Token Watch tenant setup is complete.
  • Microsoft admin consent has been granted.
  • Applications have synced and monitoring is enabled.
  • Webhook delivery is available in your Token Watch plan.
  • You have a public HTTPS endpoint that accepts POST with application/json.
  • You can store the Token Watch signing secret securely and verify X-TW-Signature.
Your webhook endpoint has to be a public HTTPS URL that Token Watch can reach from the internet.

Step-by-step setup

  1. Stand up a public HTTPS receiver or automation endpoint.
  2. In Token Watch, open Monitoring > Webhook reporting.
  3. Select the Custom webhook channel — the raw JSON option, as opposed to the Slack or Teams formats.
  4. Paste the endpoint URL.
  5. Generate the signing secret and copy it right away — it is shown only once.
  6. Store the signing secret in a secret manager or protected application setting.
  7. Verify X-TW-Signature before parsing JSON.
  8. Return a 2xx once you have accepted the message.
  9. Send a test webhook from Token Watch and confirm your receiver logs the sample payload.

Token Watch makes up to three delivery attempts, with backoff before the second and third. Any non-2xx response counts as a failure, so do not block the response on slow work. The durable pattern is the familiar one: accept the payload, enqueue it, return 204, and let a worker handle the ticket creation or SIEM write afterward.

Example payload

{
  "reportId": "f4b4f795-5c02-4d3d-b2f4-35e66df77c18",
  "tenantExternalId": "11111111-2222-3333-4444-555555555555",
  "generatedAt": "2026-06-13T00:00:00Z",
  "itemCount": 2,
  "items": [
    {
      "applicationName": "Contoso API",
      "secretName": "client-secret",
      "expiresAt": "2026-06-20T00:00:00Z",
      "status": "Expiring"
    },
    {
      "applicationName": "Fabrikam Web",
      "secretName": "signing-cert",
      "expiresAt": "2026-06-11T00:00:00Z",
      "status": "Expired"
    }
  ]
}

The status field is currently either Expired or Expiring. Deduplicate on reportId for the whole delivery, and combine it with applicationName and secretName if you fan it out into one alert per credential.

Security and signature verification

Every request on this channel carries an X-TW-Signature header shaped like ts=1781308800;h1=<hex-hmac-sha256>. To verify it, compute HMAC-SHA256 with your signing secret over <timestamp>:<raw request body>. The detail that trips most people up: use the raw body, byte for byte. Parse it and re-serialize before checking, and the bytes shift just enough to break the hash.

import crypto from "crypto";
import express from "express";

const app = express();
const signingSecret = process.env.TOKEN_WATCH_WEBHOOK_SECRET!;

app.post(
  "/webhooks/token-watch",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const signature = req.header("X-TW-Signature");

    if (!signature || !verifyTokenWatchSignature(signature, rawBody, signingSecret)) {
      return res.status(401).send("invalid signature");
    }

    const payload = JSON.parse(rawBody);
    console.log(`Token Watch alert ${payload.reportId}: ${payload.itemCount} item(s)`);

    return res.status(204).send();
  }
);

function verifyTokenWatchSignature(header: string, rawBody: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(";").map((part) => {
      const [key, ...rest] = part.trim().split("=");
      return [key, rest.join("=")];
    })
  );

  const ts = parts.ts;
  const h1 = parts.h1;
  if (!ts || !h1) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}:${rawBody}`, "utf8")
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(h1, "hex"));
}

In production, reject the request before you parse the JSON if the signature does not match, and consider rejecting stale timestamps to cut down on replay risk. Keep the signing secret and the raw X-TW-Signature value out of your logs.

Routing ideas

  • Route Expired credentials to an incident queue or high-priority ticket.
  • Route Expiring credentials to a maintenance backlog.
  • Create a ticket if a credential expires in less than 7 days.
  • Deduplicate by reportId, applicationName, and secretName.
  • Route by tenant, app naming convention, owner mapping, service catalog, or CMDB lookup.
  • Avoid creating duplicate tickets every night for the same credential.

Troubleshooting

  • No webhook arrived: confirm webhook delivery is enabled and the day's monitoring run found at least one expired or expiring tracked credential.
  • URL is invalid: make sure it is a public https:// URL.
  • 401 or 403: check signature verification, auth middleware, and whether a proxy removed X-TW-Signature.
  • 404: confirm the saved URL matches the route your app exposes.
  • 429: accept quickly and move work to a queue, or raise receiver limits.
  • Signature fails: verify the raw body, the signing secret, and the timestamp:body HMAC input order.
  • Health is unhealthy: health is based on recent scheduled deliveries, not manual tests, so a successful test may not clear it right away.

Limitations

  • Token Watch does not renew credentials for you.
  • Token Watch does not send secret values or certificate private keys.
  • A JSON webhook gives you signed delivery, but your team still needs ownership and renewal processes.
  • Slack and Microsoft Teams modes are better for human-readable notifications, not signed ingestion.

FAQ

Azure itself won't, but Token Watch will. It watches your Entra App Registration credentials and POSTs a signed JSON alert to your endpoint whenever a tracked secret or certificate is expired or about to expire.

Connect Token Watch with Microsoft admin consent, sync applications, enable monitoring, and configure email or webhook reporting.

No — it can't. Microsoft Graph never returns secret values after creation, so Token Watch only ever sees and sends metadata: names, expiry dates, status, tenant ID, and report ID.

Yes. JSON webhook requests include X-TW-Signature, signed with HMAC-SHA256 over <timestamp>:<raw body>.

Most likely nothing was wrong. Empty alerts are never sent, so if no tracked credential was expired or expiring on that day's run, there is simply no delivery.

Use Slack or Teams for human-readable team notifications. Use a JSON webhook when another system needs to process, verify, deduplicate, and route the alert.

Back to all guides

Top