# Drata Webhooks

Drata doesn't have webhook subscriptions. A webhook is a **step** inside a Drata
workflow: **Settings → Workflows → Create Workflow → add a "Send webhook" step**.
When the workflow's trigger fires (for example, evidence linked to a control),
Drata sends a `POST` to the URL you entered, with the headers and properties
you configured.

This differs from most providers in three ways:

1. **There is no documented signature.** Drata's help center describes
  authentication "as custom headers" and nothing else. It mentions that Drata
  uses Svix to send webhooks, but it documents no `svix-*` headers and no
  `whsec_` signing secret, so don't build verification around them.
2. **You choose what's in the body.** You pick the control and trigger
  properties to include. Every body has the base properties `event_type`,
  `timestamp`, `organization_id` and `workspace`.
3. **You choose the headers.** Auth headers and extra context headers are set
  per step.

## When to Use This Skill

* How do I receive Drata webhooks?
* How do I verify Drata webhook signatures? (You can't. Send a shared secret in a custom header instead.)
* How do I set up the "Send webhook" step in a Drata workflow?
* What does a Drata webhook payload look like? (`event_type`, `timestamp`, `control_*` fields.)
* How do I handle `CONTROL_EVIDENCE_LINKED` or `CONTROL_TEST_CHANGED_TO_FAIL`?
* Why does my Drata webhook arrive more than once? (Failed steps are retried up to three times.)

## Verification (core): a shared secret custom header

In the Send webhook step, add a custom header with a long random value:

```
X-Webhook-Secret: 3f9c0e...a long random hex value...

```

The header name is your choice. Check it in constant time before you parse the body:

```javascript
const crypto = require('crypto');

// NOT a Drata signature. This is a secret YOU put in the step's custom headers.
function verifyDrataSecret(headerValue) {
  const expected = process.env.DRATA_WEBHOOK_SECRET;
  if (!expected) throw new Error('DRATA_WEBHOOK_SECRET is not set'); // fail closed
  if (typeof headerValue !== 'string') return false;
  const a = Buffer.from(headerValue);
  const b = Buffer.from(expected);
  // Length check first: timingSafeEqual throws on a length mismatch.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: verifyDrataSecret(req.get('X-Webhook-Secret'))

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/drata-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/drata-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/drata-webhooks/examples/fastapi/).

Return `401` for a missing or wrong secret and `500` when the secret isn't
configured, so the endpoint never accepts everything by accident. Read the body
raw (`express.raw({ type: '*/*' })`) and parse JSON only after the check. See
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/drata-webhooks/references/verification.md).

## Payload

Drata documents the body as three groups of properties:

* **Base (always):** `event_type`, `timestamp`, `organization_id`, `workspace`
* **Control properties (the ones you select):** `control_name`, `control_code`,
  `control_id`, `control_owners`, `control_readiness`, `control_description`,
  `control_approval_status`, `control_approvers`, `control_mapped_requirements`,
  `control_mapped_tests`, `control_evidence`, `control_policies`,
  `control_notes`, `control_custom_fields`, `control_workspace_name`,
  `control_workspace_id`
* **Trigger-specific:** e.g. `evidence_name` and `linked_by` for
  `CONTROL_EVIDENCE_LINKED`, `test_name` for `CONTROL_TEST_CHANGED_TO_FAIL`

Dates are ISO strings, booleans are `true`/`false`, and arrays (owners, etc.)
are JSON. Drata's help center doesn't show a full example body, so treat any
field you didn't select as absent.

## Events (`event_type` values for control workflows)

| `event_type` | Fires when |
| --- | --- |
| `CONTROL_DETAILS_UPDATED` | Control details are edited (`updated_by`) |
| `CONTROL_READINESS_CHANGED_TO_NOT_READY` | A control becomes Not ready (`date_changed`) |
| `CONTROL_READINESS_CHANGED_TO_READY` | A control becomes Ready (`date_changed`) |
| `CONTROL_EVIDENCE_LINKED` / `CONTROL_EVIDENCE_UNLINKED` | Evidence is linked or unlinked |
| `CONTROL_POLICY_LINKED` / `CONTROL_POLICY_UNLINKED` | A policy is linked or unlinked |
| `CONTROL_TEST_MAPPED` / `CONTROL_TEST_UNMAPPED` | A test is mapped or unmapped |
| `CONTROL_REQUIREMENT_MAPPED` / `CONTROL_REQUIREMENT_UNMAPPED` | A requirement is mapped or unmapped |
| `CONTROL_TEST_CHANGED_TO_PASS` / `CONTROL_TEST_CHANGED_TO_FAIL` | A mapped test starts passing or failing |
| `CONTROL_OWNER_UPDATED` | The control owner changes (`new_control_owner`, `old_control_owner`, `updated_by`) |
| `CONTROL_MARKED_IN_SCOPE` / `CONTROL_MARKED_OUT_OF_SCOPE` | Scope changes (`changed_by`, `date_changed`) |

Workflows for risks, evidence and personnel also support webhooks, but the
pages this skill is based on only list `event_type` values for control
workflows. Log unknown values and return `200`; a body with no `event_type`
gets `400`.

## Delivery, Retries and Idempotency

* Steps in a workflow run in parallel. If one fails, "Drata will automatically
  retry the failed step up to three times". Return a 2xx quickly.
* There's no delivery ID header in Drata's documentation, so dedupe on body
  fields (this skill uses `event_type` + `control_id` + `timestamp`).
* Drata doesn't document a timestamp tolerance. `timestamp` is the event time,
  not a signed value, so don't use it to reject requests.

## Environment Variables

```bash
# REQUIRED. The value you put in the step's custom header. Generate with: openssl rand -hex 32
DRATA_WEBHOOK_SECRET=
# OPTIONAL. The custom header name (case-insensitive). Default: X-Webhook-Secret
DRATA_WEBHOOK_HEADER=X-Webhook-Secret

```

## Local Development

```bash
npx hookdeck-cli listen 3000 drata --path /webhooks/drata

```

No account required. The CLI creates a guest account on first run and gives you
a public HTTPS URL to paste into the Send webhook step, plus a web UI for
inspecting requests. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/drata-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/drata-webhooks/references/overview.md) - How Send webhook works, trigger events, payload properties, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/drata-webhooks/references/setup.md) - Step-by-step workflow setup, custom header, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/drata-webhooks/references/verification.md) - Why there is no signature, the shared-secret header pattern, gotchas