Agent skill
Cyera Webhooks Skill
Receive and verify Cyera Privacy webhooks (the privacy-request product formerly DataGrail, not Cyera's DSPM platform). Use when setting up a Cyera Privacy webhook integration, debugging X-Webhook-Signature / X-Webhook-Timestamp verification, or handling data-subject privacy requests (request_type access, deletion, opt_out) plus health_check and readiness_check deliveries.
Install this skill
npx skills add hookdeck/webhook-skills --skill cyera-webhooks
Cyera Privacy (privacy.cyera.io, the privacy-request product formerly DataGrail) automates data-subject privacy requests across integrated systems. Its webhook integration POSTs privacy-request notifications (access, deletion, opt-out), health checks and readiness checks to your endpoint.
Not Cyera's DSPM platform. This skill covers the Cyera Privacy webhook integration documented at https://integrations.privacy.cyera.io/docs/integrations/webhooks.
Hookdeck setup note for the Cyera source: For Cyera Privacy's privacy-request webhooks. Paste the Webhook Secret from the webhook integration.
When to Use This Skill
- How do I receive Cyera Privacy webhooks?
- How do I verify a Cyera webhook signature (
X-Webhook-Signature)? - Why does my Cyera signature verification fail?
- How do I handle
access,deletionandopt_outprivacy requests? - How should I answer Cyera's
health_checkandreadiness_checkrequests? - What status code does Cyera expect (
201vs200)?
Verification (core)
Cyera sends X-Webhook-Signature (the bare, padded, standard-base64 HMAC-SHA256 digest, no prefix) and X-Webhook-Timestamp. The signed message is {X-Webhook-Timestamp}:{raw body} with a colon separator, keyed with the Webhook Secret's UTF-8 bytes (not hex- or base64-decoded). Build it as bytes and never parse the body first.
const crypto = require('crypto');
function verifyCyera(rawBody /* Buffer */, headers, secret) {
const sig = headers['x-webhook-signature'];
const ts = headers['x-webhook-timestamp'];
if (!sig || !ts || !/^[A-Za-z0-9+/]{43}=$/.test(sig)) return false; // validate before decoding
const expected = crypto
.createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(Buffer.concat([Buffer.from(ts, 'latin1'), Buffer.from(':'), rawBody]))
.digest();
const received = Buffer.from(sig, 'base64');
if (received.toString('base64') !== sig) return false; // non-canonical encoding
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Python equivalent: hmac.new(secret.encode(), ts.encode('latin-1') + b':' + raw, hashlib.sha256).digest(), base64.b64decode(sig, validate=True), a re-encode check that it gives back sig (decoders ignore stray low bits in the last character; Hookdeck compares base64 exactly), then hmac.compare_digest on bytes.
- Missing or empty signature or timestamp, malformed signature, or mismatch: respond
401. - Read the raw body (
express.raw({ type: '*/*' }),await request.arrayBuffer(),await request.body()); parse JSON only after the check. - Compare the whole header as one digest: no prefix, no list splitting.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Timestamp window (opt-in)
Do not reject old timestamps by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck itself applies no tolerance, so a fixed window would reject legitimate redeliveries. The examples offer an opt-in CYERA_ (unset or 0 = off; X-Webhook-Timestamp is Unix seconds). Cyera's webhook docs describe no freshness window.
Event Types
Dispatch on the body's request_type:
request_type | Meaning | Respond with |
|---|---|---|
health_check | Liveness ping | 200 |
access | Data-subject access request | 201 Created |
deletion | Data-subject deletion request | 201 Created |
opt_out | Opt-out request (topic: opt_out:create) | 201 Created |
readiness_check | Asks the readiness question configured on the integration; answer it with a callback | 200 (the docs give no code for readiness checks) |
Access, deletion and opt-out bodies carry topic (e.g. privacy_request:create), identifiers, webhook_callback_url and request ids (privacy_request_id or opt_out_request_id, plus webhook_request_id). Use 4xx/5xx only when the request was not created. Completion is reported later by a PUT to webhook_callback_url with an API key (outside the scope of this skill). Retries are not documented.
Setup in Hookdeck
For Cyera Privacy's privacy-request webhooks. Paste the Webhook Secret from the webhook integration.
The Hookdeck source type is CYERA (labelled "Cyera"), and it manages the method as POST.
Environment Variables
CYERA_WEBHOOK_SECRET=dgwebhook_your_webhook_secret # Webhook Secret from the integration
# CYERA_WEBHOOK_TOLERANCE_SECONDS=300 # optional; unset or 0 = off
Local Development
npx hookdeck-cli listen 3000 cyera --path /webhooks/cyera
No account required: the CLI creates a guest account on first run and gives you a public URL plus a web UI for inspecting requests. Paste the URL into the Cyera Privacy webhook integration.
Reference Materials
- references/overview.md - What the webhooks are, request types, payloads, responses
- references/setup.md - Create the webhook integration, get the Webhook Secret, point it at your endpoint
- references/verification.md - Byte-exact signature scheme, gotchas, debugging