Receive events with webhooks
From Teams up
From Teams up, we send a signed JSON event to your HTTPS endpoint when we store a report, when an alert opens or clears, and when an export started through the API is ready. Your receiver checks the signature, answers with a 2xx status and then does its work.
Add an endpoint
An admin adds endpoints under Settings → Developer → Webhooks. The plan sets how many:
| Plan | Endpoints |
|---|---|
| Starter | 0 |
| Pro | 0 |
| Teams | 10 |
| Enterprise | 25 |
- Write a description, such as the name of the system that receives the events.
- Give the endpoint URL: an
https://address on the public internet, with a host name and not an IP address, and with no user name in it. - Tick the events the endpoint gets. All of them are ticked at first.
- Press Add endpoint.
The next page shows the endpoint's signing secret once. It starts with whsec_. Put it in your receiver, then press Send test on the endpoint's page. The test event, webhook.test, goes to that endpoint only. We try it once and never retry it.
An endpoint gets every event it is ticked for, for every domain of the organization.
The events
| Event | When we send it | Fields of data |
|---|---|---|
report.received | We stored an aggregate, TLS or forensic report for one of the organization's domains. A forensic report is named by its ids only, never by its content. | id kind domain reporter report_id begin end received_at counts url raw_url |
alert.fired | An alert opened, or something happened that alerts report. | id domain subject kind severity fired_at resolved_at headline text action params url |
alert.resolved | An open alert cleared. Its id is the id of the alert.fired event. | id domain subject kind severity fired_at resolved_at headline text action params url |
export.ready | An export started through the API is ready to download. | id kind status domain from to header_from report_kinds created_at expires_at size_bytes download_url |
webhook.test | Sent to one endpoint when an admin presses Send test. | organization endpoint |
Every event has the same body:
{
"type": "export.ready",
"timestamp": "2026-10-27T07:41:12Z",
"data": { … }
}
type names the event, timestamp says when we made it, in UTC, and data holds the fields in the table. The OpenAPI document describes each event's data in full under webhooks, and the endpoint reference gives the document's address.
Check the signature
Each POST carries three headers, signed the Standard Webhooks way:
webhook-id: the event's id, such asmsg_1234. Every retry of the event carries the same id.webhook-timestamp: when this attempt was made, in Unix seconds.webhook-signature:v1,followed by an HMAC-SHA256 signature in base64. For a while after you rotate the secret, it holds two signatures, separated by a space.
The signature covers the id, the timestamp and the body, joined with dots. Its key is the secret without whsec_, decoded from base64. Any Standard Webhooks library checks it. Without one, this Python does the same with the standard library:
import base64
import hashlib
import hmac
import time
def verify(secret: str, headers: dict, body: bytes, tolerance: int = 300) -> bool:
"""True when a signature matches and the timestamp is recent."""
msg_id = headers.get("webhook-id", "")
timestamp = headers.get("webhook-timestamp", "")
signatures = headers.get("webhook-signature", "")
if not (msg_id and timestamp and signatures):
return False
try:
sent_at = int(timestamp)
except ValueError:
return False
if abs(time.time() - sent_at) > tolerance:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = msg_id.encode() + b"." + timestamp.encode() + b"." + body
digest = hmac.new(key, signed, hashlib.sha256).digest()
expected = base64.b64encode(digest).decode()
for signature in signatures.split():
version, _, value = signature.partition(",")
if version == "v1" and hmac.compare_digest(value.encode(), expected.encode()):
return True
return False
Pass it the body as raw bytes, before any JSON parsing, because the signature covers those exact bytes. Pass the headers as your web framework gives them, or with their names in lower case. Refuse the POST when it returns False. The example also refuses a timestamp more than tolerance seconds away from your clock, so a recorded request cannot be sent to you again later.
Answer quickly
Answer with any 2xx status within 5 seconds. Anything else counts as a failure: another status, no answer in time, or a redirect, which we do not follow. Do slow work after you have answered.
- Events can arrive out of order, and a retry can arrive after a newer event. Order them by
timestamp. - Use
webhook-idto drop an event you already have.
When delivery fails
After a failed attempt we wait and try again. The waits are 1 minute, 10 minutes, 1 hour, 6 hours and 24 hours, in that order. When the last attempt fails too, the delivery is marked failed and we stop.
The endpoint's page has a Delivery log of every event we sent or tried to send to it in the last 90 days, with the status your receiver answered and the error.
When every delivery to an endpoint has failed for 3 days, we turn the endpoint off and email the admins. Events from while it is off are never sent. Fix the receiver, then press Turn on on the endpoint's page.
Rotate the secret
Rotate secret on the endpoint's page makes a new secret at once and shows it once. The old secret keeps working for 24 hours. During that time every POST carries two signatures, one per secret, and a match with either is enough. Put the new secret in your receiver before the time runs out. Rotating again within that time drops the oldest secret.
When the plan changes
- A plan with fewer endpoints. The oldest endpoints keep receiving events, up to the new plan's count, and the newer ones pause. A paused endpoint receives again when an older one is turned off or deleted, or when the plan includes more endpoints.
- A plan without webhooks. We stop sending, and events that wait for a retry are dropped. We keep the endpoints and their secrets. They work again, with the same secrets, when the organization is back on a plan from Teams up.
An endpoint or an alert channel?
Alerts can also go to a webhook channel under Settings → Notifications, from Teams up. A channel sends only the alerts you choose, kind by kind. Its events are alert.opened, alert.resolved and alert.test, signed the same way. So an alert that opens is alert.fired at an endpoint and alert.opened in a channel. An endpoint gets every event you tick, for every domain, reports and exports included.
Published 27 October 2026 · Checked 27 October 2026