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, deletion and opt_out privacy requests?
  • How should I answer Cyera's health_check and readiness_check requests?
  • What status code does Cyera expect (201 vs 200)?

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_WEBHOOK_TOLERANCE_SECONDS (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_typeMeaningRespond with
health_checkLiveness ping200
accessData-subject access request201 Created
deletionData-subject deletion request201 Created
opt_outOpt-out request (topic: opt_out:create)201 Created
readiness_checkAsks the readiness question configured on the integration; answer it with a callback200 (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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →