# 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.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cyera-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cyera-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cyera-webhooks/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_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

```bash
CYERA_WEBHOOK_SECRET=dgwebhook_your_webhook_secret   # Webhook Secret from the integration
# CYERA_WEBHOOK_TOLERANCE_SECONDS=300                # optional; unset or 0 = off

```

## Local Development

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/cyera-webhooks/references/overview.md) - What the webhooks are, request types, payloads, responses
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cyera-webhooks/references/setup.md) - Create the webhook integration, get the Webhook Secret, point it at your endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cyera-webhooks/references/verification.md) - Byte-exact signature scheme, gotchas, debugging