Agent skill
Drata Webhooks Skill
Receive and authenticate Drata workflow webhooks sent by the "Send webhook" step. Use when building a Drata webhook receiver, securing it with a shared secret custom header (Drata documents no signing secret), handling event_type values like CONTROL_EVIDENCE_LINKED, CONTROL_READINESS_CHANGED_TO_NOT_READY or CONTROL_TEST_CHANGED_TO_FAIL, or debugging Drata compliance automation webhook deliveries.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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:
- 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 nowhsec_signing secret, so don't build verification around them. - 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_idandworkspace. - 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_LINKEDorCONTROL_?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:
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/, examples/nextjs/, 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.
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_nameandlinked_byforCONTROL_EVIDENCE_LINKED,test_nameforCONTROL_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_ | A control becomes Not ready (date_changed) |
CONTROL_ | A control becomes Ready (date_changed) |
CONTROL_EVIDENCE_LINKED / CONTROL_ | 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_ / CONTROL_ | A requirement is mapped or unmapped |
CONTROL_ / CONTROL_ | 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_ | 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.
timestampis the event time, not a signed value, so don't use it to reject requests.
Environment Variables
# 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
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.
Reference Materials
- references/overview.md - How Send webhook works, trigger events, payload properties, retries
- references/setup.md - Step-by-step workflow setup, custom header, testing
- references/verification.md - Why there is no signature, the shared-secret header pattern, gotchas