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
POSTwithapplication/json. - You can store the Token Watch signing secret securely and verify
X-TW-Signature.
Step-by-step setup
- Stand up a public HTTPS receiver or automation endpoint.
- In Token Watch, open Monitoring > Webhook reporting.
- Select the Custom webhook channel — the raw JSON option, as opposed to the Slack or Teams formats.
- Paste the endpoint URL.
- Generate the signing secret and copy it right away — it is shown only once.
- Store the signing secret in a secret manager or protected application setting.
- Verify
X-TW-Signaturebefore parsing JSON. - Return a
2xxonce you have accepted the message. - 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
Expiredcredentials to an incident queue or high-priority ticket. - Route
Expiringcredentials to a maintenance backlog. - Create a ticket if a credential expires in less than 7 days.
- Deduplicate by
reportId,applicationName, andsecretName. - 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:bodyHMAC 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
X-TW-Signature, signed with HMAC-SHA256 over <timestamp>:<raw body>.
Token Watch