Skip to main content

Webhooks

In an organization, each webhook belongs to one client. When Profundis detects new or changed assets for that client, the alert goes to the webhook of that client only. If one person or team handles each client, each of them gets the alerts of their own clients in their own channel.

Who can set up a webhook?​

  • Owners and admins manage the webhooks of every client, from Organization settings → Webhooks.
  • An analyst manages the webhooks of the clients they have access to, from the Webhooks tab of that client.
  • Client accounts and API keys cannot manage webhooks.

Saving a webhook or rotating its secret asks you to have signed in recently.

If the person who set a webhook's destination loses access to its client (access removed, role changed, or they leave the organization), the webhook is paused. Someone who still has access can check the destination and resume it.

How many webhooks can I have?​

PlanActive webhooks
Trial1
Teams3
Business10

The limit counts active webhooks across all clients of the organization. Paused and revoked webhooks do not count. An organization can keep up to 100 webhooks that are not revoked.

Which destination should I choose?​

DestinationWhat you pasteWhat is posted
SlackAn incoming webhook URL, https://hooks.slack.com/services/…A message in the channel
Microsoft TeamsThe URL of a Workflows "When a Teams webhook request is received" triggerAn Adaptive Card in the channel
DiscordA channel webhook URL, https://discord.com/api/webhooks/…A message in the channel
Signed JSONAny public HTTPS URL on port 443 that you runThe JSON event, signed with a secret

Slack, Teams and Discord accept only their own webhook URLs. Signed JSON rejects local, private and reserved addresses, including hostnames that resolve to one; the check runs again each time a request connects. Redirects are not followed.

Which events can I receive?​

EventWhen it is sent
watch.changes_detectedA monitoring run of the client found at least one new or changed asset that is not suppressed. This is the monitoring alert.
watch.completedA monitoring run finished, with or without changes, successfully or not.
job.completedA search or tool job of the client finished.
report.publishedA report of the client was published.
budget.threshold_reachedThe client's credit envelope reached a threshold. Owners and admins only.
audit.key_changedAn API key with access to the client was created, changed or revoked. Owners and admins only.

New webhooks subscribe to watch.changes_detected only. The first run of a monitoring task records what already exists and sends no alert.

What does a monitoring alert contain?​

It names the client and the monitoring task, gives the number of changes and lists up to 50 of them (new asset, changed asset or certificate expiring soon), with a link to the monitoring page. The list shows the observed name or value of each asset. Slack, Teams and Discord messages are shortened to fit the provider's size limits; the count and the link are always kept.

The other events carry only identifiers and states. They never contain queries, evidence or report content.

How do I verify a signed JSON event?​

When you create a signed JSON webhook, its signing secret is shown once. Store it in your receiver's secret store.

Each request is a POST with Content-Type: application/json and these headers:

HeaderValue
X-Profundis-Event-IDThe event ID. It is the same on every retry: use it to ignore duplicates.
X-Profundis-Signaturet=<unix timestamp>,v1=<hex HMAC-SHA256>

The signature is the HMAC-SHA256, with your secret as the key, of the timestamp, a dot, and the exact request body bytes. Compute it on the raw body before parsing the JSON, compare in constant time, and reject timestamps older than a few minutes.

import hashlib
import hmac
import time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
try:
parts = [p.split("=", 1) for p in header.split(",")]
timestamp = next(v for k, v in parts if k == "t")
signatures = [v for k, v in parts if k == "v1"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
except (StopIteration, ValueError): # malformed header
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)

After you rotate the secret, requests carry two v1 values for 24 hours, one per secret, so your receiver keeps working while you update it. You cannot rotate again until those 24 hours are over.

An event looks like this:

{
"schema_version": 1,
"id": "8f0c7d4e-6b1a-4f7e-9a51-2c3d4e5f6a7b",
"type": "watch.changes_detected",
"organization_id": "…",
"client_id": "…",
"client_name": "Acme",
"resource_id": "8f0c7d4e-6b1a-4f7e-9a51-2c3d4e5f6a7b",
"state": "changes_detected",
"occurred_at": "2026-09-25T09:00:00Z",
"data": {
"watch_id": "…",
"watch_name": "Acme certificates",
"dataset": "vhosts",
"run_id": "8f0c7d4e-6b1a-4f7e-9a51-2c3d4e5f6a7b",
"count": 2,
"changes": [
{"id": "…", "kind": "first_observed", "identity": "…", "label": "login.acme.example"},
{"id": "…", "kind": "attributes_changed", "identity": "…", "label": "mail.acme.example"}
],
"truncated": false,
"url": "https://profundis.io/alerting?workspace=…&client=…"
}
}

Only watch.changes_detected has a data object. New fields may be added to schema_version 1; ignore the ones you don't know.

What happens when my endpoint is down?​

  • A 2xx response counts as delivered. The response body is ignored.
  • Connection errors, timeouts (10 seconds), 408, 425, 429 and 5xx responses are retried with an increasing delay, up to 8 attempts within 24 hours. A Retry-After header is followed, up to one hour.
  • A 410 response pauses the webhook. For Slack, Teams and Discord, 401, 403 and 404 also pause it, since that is how they answer for a deleted webhook.
  • Any other response fails the delivery without a retry.
  • If 1,000 events are waiting for one webhook, it is paused.

Each webhook has a Delivery history with every attempt and its result. A failed delivery can be retried from there within its 24 hours. Events are not replayed after a pause: resume the webhook, then check the monitoring page for what you missed.

Miscellaneous​

How do I check that my webhook works?​

Use Send test event on the webhook. It sends an endpoint.test event with no customer data. You can send one test per minute.

I changed the URL and some events were not delivered​

Changing a webhook's destination or settings cancels the events still waiting for it. Rotating the secret keeps them.

I switched a webhook from Slack to signed JSON​

A new signing secret is shown once when you save. Slack, Teams and Discord webhooks don't use one.