Docs

Webhooks

Configure outbound webhooks at the org level. Flowcerta POSTs an HMAC-signed JSON envelope to your endpoint whenever a subscribed event fires, so governance findings flow into Slack, Teams, Jira, ServiceNow, or any system that accepts a webhook.

Configure a webhook

Org admins create webhooks via POST /api/v1/org-webhooks. The response includes a generated secret — this is the only time the full secret is returned. Store it somewhere safe; subsequent reads return a secret_preview instead.

A dashboard UI for managing webhooks is on the roadmap; until then, the JSON API is the supported configuration surface.

Delivery headers

HeaderValue
Content-Typeapplication/json
X-Flowcerta-EventThe event name, e.g. validation.completed.
X-Flowcerta-DeliveryUnique delivery ID. Use this to dedupe replays.
X-Flowcerta-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body keyed by the webhook's secret.
X-Flowcerta-TargetThe configured target adapter: generic, slack, orteams. Receivers can ignore this — the body shape already matches.

Verify the signature

Recompute the HMAC over the raw request body and compare in constant time. Receivers should reject any request whose computed signature does not match the header.

Node.js

const crypto = require('crypto')

// Inside your webhook handler (Express)
app.post('/flowcerta-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('X-Flowcerta-Signature') ?? ''
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.FLOWCERTA_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).end()
  }

  const event = JSON.parse(req.body.toString('utf8'))
  console.log(event.event, event.data)
  res.status(200).end()
})

Python

import hmac, hashlib, os
from flask import request

def verify_flowcerta_webhook(req):
    signature = req.headers.get('X-Flowcerta-Signature', '')
    expected = 'sha256=' + hmac.new(
        os.environ['FLOWCERTA_WEBHOOK_SECRET'].encode('utf-8'),
        req.get_data(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Event reference

validation.completed

Fired after every authenticated validation finishes — pass or fail. Payload mirrors what the API would return, scoped to the top 10 findings (ordered by severity) so your receiver doesn't get an oversized message card. Pull the full result via GET /api/v1/results/{validationId} when you need the rest.

{
  "event": "validation.completed",
  "orgId": "00000000-0000-0000-0000-000000000000",
  "occurredAt": "2026-05-15T18:42:11.532Z",
  "data": {
    "validationId": "11111111-1111-1111-1111-111111111111",
    "platform": "uipath",
    "fileName": "Main.xaml",
    "label": null,
    "passed": false,
    "score": 47,
    "enforcementMode": "blocking",
    "policyPackSlug": "uipath-reframework-baseline",
    "validatedAt": "2026-05-15T18:42:11.500Z",
    "summary": {
      "critical": 1,
      "high":     3,
      "medium":   2,
      "low":      4,
      "total":   10
    },
    "findings": [
      {
        "ruleId":      "FC-UIP-HCD-001",
        "severity":    "critical",
        "category":    "hardcoded_value",
        "location":    "Main.xaml:42",
        "description": "Hardcoded credential or sensitive value detected."
      }
      // …up to 10 findings, ordered by severity
    ]
  }
}

validation.below_threshold

Fired when a validation's health score falls under the score-threshold alert configured on the org. The payload includes the workflow file, the score, and the configured threshold so receivers can format an actionable alert without a follow-up API call.

exception.expiring_soon

Fired once per approved risk exception when it enters the warning window (default 7 days before expiresAt). A daily cron does the scan and stamps each row after dispatch, so each exception fires at most once. Payload includes the rule, the workflow file, the original reason, and daysRemaining so a receiver can build a ticket or Slack post that points the owner at the renewal step.

{
  "event": "exception.expiring_soon",
  "orgId": "00000000-0000-0000-0000-000000000000",
  "occurredAt": "2026-05-22T04:00:00.000Z",
  "data": {
    "exceptionId":      "22222222-2222-2222-2222-222222222222",
    "ruleId":           "FC-UIP-HCD-001",
    "fileName":         "Main.xaml",
    "location":         "Main.xaml:42",
    "reason":           "Documented credential injected at runtime by Orchestrator.",
    "expiresAt":        "2026-05-28T00:00:00.000Z",
    "daysRemaining":    6,
    "requestedByEmail": "ops-lead@example.com"
  }
}

Routing webhooks into Slack and Teams

Set target: "slack" or target: "teams" when you create the webhook and Flowcerta posts a body the destination already knows how to render — Slack Block Kit messages and Teams MessageCards respectively. TheX-Flowcerta-Signature header is still set over the formatted bytes for consistency; Slack and Teams ignore it. For everything else (Jira, ServiceNow, custom), stick with the default generic target and translate the envelope in a thin receiver.

Need help wiring up your specific stack? Talk to us.