Outbound webhooks

Vaks PM · Integration guide · Webhooks · July 2026

What you will end up with. Vaks PM calling an HTTP endpoint of yours whenever something happens — a task created, a comment posted, a project activated — with each request signed so your receiver can prove it came from Vaks PM. Deliveries are retried, logged and replayable. A built-in Microsoft Teams card format means you can post to a channel without writing any code at all.

What it does — and what it does not

Events you can subscribe to

Twenty-seven events, plus the wildcard * which subscribes to all of them — including any added in future versions.

FamilyEvents
Taskstask.created · task.updated · task.deleted · task.moved · task.assigned · task.unassigned · task.completed
Agent workflowtask.claimed · task.released · task.review_requested · task.changes_requested · task.approved · deliverable.submitted · deliverable.reviewed
Projectsproject.created · project.updated · project.activated · project.announcement
Collaborationcomment.created
Skillsskill_request.created · skill_request.acknowledged · skill_request.resolved · skill_request.rejected · skill_request.reminded
Operationsaudit.alert · backup.failed · automation.alert
The three operational events only reach organization-scoped webhooks. audit.alert (repeated failed sign-ins), backup.failed and automation.alert are not tied to a project, so a project-scoped or team-scoped webhook never receives them. project.activated is worth knowing about too: it fires on the transition to active and is the signal an external provisioner should wait for.

Prerequisites

SideWhat you need
Your sideAn HTTPS endpoint that answers 2xx quickly. Anything else counts as a failure. It must be publicly resolvable — private, loopback and link-local addresses are refused, see governance. For a Teams channel, a Power Automate flow is enough; no code.
Vaks PMorg:manage for organization-wide webhooks. Project managers can create webhooks for their own project without being administrators — see scope & delegation.

Step 1 — Create the endpoint

Open Admin → Integrations → Webhooks → Endpoints and press New webhook.

FieldWhat to put
LabelFree text for your own benefit, shown in the list and the delivery log.
URLYour receiver. HTTPS strongly preferred — http:// is accepted but sends the payload in clear. Only ports 80, 443, 8080 and 8443 are allowed.
EventsTick what you want. Prefer naming the events you handle over subscribing to everything: the wildcard will deliver future event types your receiver has never seen.
ScopeWhole org, A project or A team. See below.
Payload formatJSON (raw) for your own integrations, Teams (Adaptive Card) to post into a channel. See the Teams format.
SecretLeave empty and one is generated for you. This is the key your receiver uses to verify signatures.
ActiveTick to start delivering.
The secret stays readable to administrators. Unlike API tokens, it is not hidden after creation — the list has a Show toggle, so anyone with access to this screen can retrieve it at any time. Treat it as a shared secret between two systems, not as a credential belonging to a person. Rotate replaces it instantly with no overlap window: the old secret stops signing on the very next delivery, so update your receiver in the same maintenance window.

Step 2 — Verify the signature

Every request carries an HMAC-SHA256 signature of the body, computed with your webhook's secret. Verifying it is what makes the endpoint safe to expose: without it, anyone who guesses your URL can post fake events.

Two details decide whether your implementation is correct:

// Node.js / Express — note express.raw, not express.json
const crypto = require('crypto');

app.post('/vaks-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected =
    'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const got = req.get('X-Vaks-Signature') || '';

  const a = Buffer.from(expected), b = Buffer.from(got);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate: the same deliveryId may arrive more than once.
  if (alreadyHandled(req.get('X-Vaks-Delivery'))) return res.status(200).end();

  handle(event);
  res.status(200).end();   // answer 2xx fast; do the work asynchronously
});
The signature does not cover the timestamp, so it proves authenticity but not freshness — a captured request could in principle be replayed against you. Two consequences: deduplicate on X-Vaks-Delivery, which is stable across retries of the same delivery, and treat X-Vaks-Timestamp as informational only. If replay matters in your threat model, reject deliveries whose timestamp is far from now, in addition to checking the signature.

Step 3 — Test

Press Send test on the webhook's row. This sends a ping event immediately, regardless of which events you subscribed to, so it always works as a connectivity check.

Then open Recent deliveries below the list and confirm the row reads SUCCESS with a 2xx response. If it does not, expand Details — the error, the exact payload sent, and your receiver's response body are all recorded there. That panel is the single most useful debugging tool on this page.

The ping envelope carries the event name twice, once at envelope level and once inside payload. That is expected and only true for the test event.

Payload & headers

In raw format, every request has the same envelope, with the event-specific data nested under payload:

{
  "event": "task.created",
  "deliveryId": "9f1c…",
  "organizationId": "…",
  "at": "2026-07-19T21:04:11.482Z",
  "payload": { }
}
HeaderMeaning
X-Vaks-EventThe event name, so you can route without parsing the body.
X-Vaks-DeliveryDelivery identifier. Stable across every retry of the same delivery — this is your deduplication key.
X-Vaks-Signaturesha256= followed by the lowercase hex HMAC of the raw body.
X-Vaks-TimestampSend time, epoch seconds. Regenerated on each retry, and not covered by the signature.
User-AgentVaks-PM-Webhook/1.0

Payload shapes vary by event and are best discovered from a real delivery: subscribe, trigger the action, and read the recorded payload in the delivery log. Build your receiver defensively — treat fields as optional and ignore what you do not recognise, since payloads gain fields between versions.

Retries & failure

A webhook that keeps failing is switched off automatically. After 5 consecutive deliveries that exhaust all their attempts — 25 failed requests in total — the webhook is deactivated and flagged ⚠︎ auto-disabled in the list. Any success resets the counter. To bring it back, edit the webhook and tick Active; that also clears the counter. Nothing is delivered in the meantime and there is no catch-up: events that occurred while it was disabled are lost, not queued.

Microsoft Teams format

Choosing Teams (Adaptive Card) replaces the JSON envelope with a Teams message wrapping an Adaptive Card. The body Vaks PM sends looks like this:

{
  "type": "message",
  "attachments": [{
    "contentType": "application/vnd.microsoft.card.adaptive",
    "content": { /* an Adaptive Card: title, headline, up to 8 facts */ }
  }]
}

That shape is exactly what Teams expects. The card shows a title for the event, a one-line headline drawn from the payload, and up to eight facts when present: status, priority, assignee, author, due date, team, skill and project. There are two ways to get it into a channel — pick one.

Option A — Teams Workflows / Power Automate (recommended)

This is Microsoft's current path and the one that will keep working. Microsoft is retiring the older incoming-webhook connectors (Option B), so prefer this for anything new.

  1. In the target Teams channel, open the menu → Workflows (or go to Power Automate directly). Choose the template "Post to a channel when a webhook request is received".
  2. Pick the team and channel to post into, and create the flow. Its trigger — "When a Teams webhook request is received" — generates a callback URL. Copy it.
  3. In Vaks PM, create a webhook (or edit one): set Payload format to Teams (Adaptive Card), paste the callback URL into URL, and tick the events you want.
  4. Back in the flow, make sure the posting action receives the Adaptive Card from the request body. Vaks PM sends the card under attachments[0].content; if the template does not already reference it, point the "Post card in a chat or channel" action at that path.
  5. Save the flow, then press Send test on the webhook in Vaks PM. A card should appear in the channel within a few seconds. If it does not, open Recent deliveries — a 2xx with no card usually means the flow ran but the card field was not wired to attachments[0].content.

Option B — Teams incoming webhook (simplest, being retired)

If your tenant still allows incoming-webhook connectors, this needs no flow at all — the body Vaks PM sends is exactly what the connector expects.

  1. In the channel, menu → ConnectorsIncoming WebhookConfigure. Name it, optionally set an icon, and Create.
  2. Copy the generated URL — it is on ….webhook.office.com.
  3. In Vaks PM, create the webhook with Payload format = Teams (Adaptive Card) and that URL. Send test; the card posts directly.
Microsoft is deprecating incoming-webhook connectors. Many tenants have already disabled them. If Incoming Webhook is missing from the Connectors list, or the URL stops working, that is why — move to Option A.
The tenant allowlist can block the URL. If you turned on Allowed Entra ID tenants (Teams) under Governance, Vaks PM checks the tenant embedded in Microsoft URLs and refuses one belonging to another tenant — this covers webhook.office.com (Option B) and Power Automate URLs. A Workflows trigger hosted elsewhere (for example on logic.azure.com) is not covered by that pin, but is subject to the Allowed domains list if you set one — add the trigger's host there. A blocked send shows in the delivery log as Blocked by governance with the reason.
Two things to expect once it works. The Vaks envelope is absent in this format — there is no deliveryId or organizationId in the body, only in the headers, so a flow that needs them must read the headers. And the card carries no link back to the item: the worker that renders it cannot resolve your public address, so a button would point nowhere. Card titles and field labels are in French in this release.

Scope & delegation

A webhook's scope decides which events reach it:

ScopeReceivesWho can create it
Whole orgEverything, including the operational events.Administrators only, always.
A projectOnly events carrying that project.Administrators, and delegated roles — see below.
A teamEvents tied to that team, plus events from the projects the team is assigned to.Administrators, and team leads under the same policy.

Delegation is controlled by Who can create a webhook, under Admin → Integrations → Webhooks → Governance:

Where allowed, project managers reach it from the project's Manage → Integrations tab, which only appears when their role permits it. Scope is forced server-side to their own project, so a delegated user cannot widen it — not at creation, and not by editing afterwards.

Allowlists & SSRF protection

Because a webhook sends your data to an address someone typed in, destinations are checked. Two settings under Governance tighten this, and both are worth turning on before delegating webhook creation to project managers:

SettingEffect
Allowed domainsWhen non-empty, only these hosts and their subdomains may be targeted. Empty means any public host.
Allowed Entra ID tenants (Teams)Applies only to Microsoft endpoints — Teams incoming webhooks and Power Automate. Vaks PM extracts the tenant identifier from the URL and refuses destinations belonging to another tenant, so data cannot be posted into somebody else's Microsoft 365.

Independently of any setting, internal targets are always refused: localhost, .local, .internal, and private, loopback or link-local address ranges. This is checked twice — when the URL is saved and again at send time, with an actual DNS resolution, which also defeats a hostname that only resolves to a private address later.

Governance applies retroactively. The rules are re-evaluated on every send, not just at save time, so tightening the allowlist immediately stops existing webhooks that no longer comply. Their deliveries are recorded as failures with the reason, rather than silently disappearing. Expect that when you introduce an allowlist on a live instance.
Custom Power Platform environments cannot be verified while the Entra tenant allowlist is active, so they are refused. If you use one, you will need to leave that allowlist empty and rely on the domain allowlist instead.

Delivery log

Recent deliveries, below the endpoint list, records every attempt: status and response code, event, target webhook, when it was queued, and how many attempts it took. Expanding a row shows the error, the payload sent and your receiver's response body (first 1000 characters).

A failed delivery can be replayed with Retry. It reuses the stored payload and the same delivery identifier, so a receiver that deduplicates correctly will recognise it — but the timestamp and signature are recomputed. The destination is re-checked against current governance first, so a retry to a now-forbidden address is refused.

The delivery log is never purged. There is no retention setting and no cleanup job, so rows accumulate for the lifetime of the instance. On a busy organization with a chatty webhook this grows steadily. Worth watching alongside your other database growth, and worth raising with your vendor if it becomes material.

Troubleshooting

SymptomCause & fix
Signature never matchesAlmost always the raw-body problem: the body was parsed and re-serialised before verification. Capture the bytes as received. Check too that you kept the sha256= prefix in the comparison.
Test succeeds, real events never arriveThe test ignores your event subscription. Confirm the events are actually ticked, and that the scope matches — a project-scoped webhook receives nothing for other projects, and never receives the operational events.
Blocked by governance in the delivery logThe destination fails a rule: not in the domain allowlist, a private address, a disallowed port, or a Microsoft URL whose tenant is not permitted. The recorded reason names which.
Everything failed at once, and the webhook is now disabled5 consecutive exhausted deliveries. Fix the receiver, then edit the webhook and tick Active. Events missed while disabled are not replayed.
Duplicate eventsExpected. Retries, and some actions emitting two events. Deduplicate on X-Vaks-Delivery and treat the pairing of task.updated with a more specific event as normal.
Deliveries stop after a rotationRotation has no overlap window. The receiver must hold the new secret before the next delivery.
Timeouts under load10 seconds per attempt, and the clock includes your processing. Acknowledge with 2xx first, then work.

What is logged

Creating, editing, deleting a webhook and rotating its secret are all audited at critical severity — a webhook is a data-exfiltration path, so changes are treated as security events. You will find them under Admin → Security & Compliance → Audit log, with the actor and the target.


Related: all integrations · operations for backup alerting, which uses the backup.failed event.