Gareth Wilson Gareth Wilson

Guide to Cyera Webhooks: Features and Best Practices

Published


Cyera webhooks hand privacy work to your systems. When a data subject asks to see their data, have it deleted or opt out, Cyera Privacy sends your endpoint the request with the identifiers you need to find that person, and waits for you to report back when the job is done. If your organisation uses Cyera Privacy to automate data-subject requests across internal systems, webhooks are how those requests reach the systems Cyera has no built-in connector for.

This guide covers how Cyera Privacy webhook integrations work, the request types and their bodies, how to verify a request, the response codes and callbacks Cyera expects, and the practices that keep a Cyera receiver reliable.

What are Cyera webhooks?

These are the webhooks of Cyera Privacy, the privacy-request product, not alerts from Cyera's data security platform. A webhook integration lets Cyera "dispatch privacy request processing to your systems". When a privacy request reaches the right stage of its lifecycle, Cyera sends a JSON POST to your endpoint. Your system does the work, such as exporting or deleting the person's data, and then confirms completion with a PUT to the callback URL in the request.

So a Cyera webhook is the first half of a two-step protocol. The response to the webhook says whether you accepted the request. The callback, sent later from your side, says how it ended.

Each request carries X-Webhook-Timestamp and X-Webhook-Signature. The signature is a base64 HMAC-SHA256 of the timestamp, a colon and the raw body, keyed with the Webhook Secret Cyera generates for the integration.

Cyera webhook features

FeatureDetails
ConfigurationA webhook integration in Cyera Privacy. The Webhook Secret is on its connection page
Request typeshealth_check, access, deletion, opt_out, readiness_check
Signature headerX-Webhook-Signature, bare standard base64
Timestamp headerX-Webhook-Timestamp, Unix seconds
SchemeHMAC-SHA256, base64
Signed content<X-Webhook-Timestamp>:<raw body>, with a colon
SecretGenerated by Cyera. The docs' examples show values starting dgwebhook
User agentprivacyclient/1.0
Expected responses201 when a request is created, 200 for health checks and reminders, 4xx or 5xx when not created
CompletionPUT to webhook_callback_url with an API key that has the Webhook Callback scope
Retries, timeout, replay window, source IPsNot documented

Common events

Cyera calls these request types rather than events. The body's request_type says which one you have:

request_typeWhat Cyera wants
accessRetrieve the data subject's personal data from your system
deletionDelete the data subject's personal data from your system
opt_outProcess an opt-out for the data subject
health_checkConfirm the endpoint is reachable. Sent periodically and not tied to a privacy request
readiness_checkAnswer a configured question, such as whether the system is ready. Sent only when a readiness check is configured on the integration

Access, deletion and opt-out bodies also carry a topic. privacy_request:create and opt_out:create are new requests, and privacy_request:reminder and opt_out:reminder are reminders about requests that haven't been completed.

Every body has request_type, integration_name, integration_kind (always webhook) and a unique webhook_request_id. Here is Cyera's access request example:

{
  "request_type": "access",
  "integration_name": "Webhook Integration",
  "integration_kind": "webhook",
  "topic": "privacy_request:create",
  "identifiers": {
    "email": [ { "email": "john@example.com" } ],
    "user_id": [ { "user_id": "100101" } ]
  },
  "webhook_callback_url": "https://acme.privacy.cyera.io/api/v2/webhooks/callback",
  "privacy_request_id": "4b7a02e2-361e-4957-9d69-b381e3f47758",
  "integration_id": "4b7a02e2-361e-4957-9d69-b381e3f47758",
  "webhook_request_id": "3aa32ca6-e60d-4b71-981a-d1ba258fd712"
}

identifiers groups values by category, each as an array: email, phone, user_id, service_id, application_id, browser_id, advertising_id and social_media_id. Opt-out bodies use opt_out_request_id and an integer integration_state_id instead of privacy_request_id and integration_id. Readiness checks add a question and its two answers, such as ["Yes", "No"]. Cyera doesn't publish a deletion example, but documents the same fields for access and deletion.

Setting up Cyera webhooks

  1. Build an endpoint that accepts JSON POST requests. Cyera sends health checks to it periodically, so it needs to be up before you connect the integration.
  2. In Cyera Privacy, create the webhook integration for your endpoint, and copy the Webhook Secret from the integration's connection page. Store it server-side, for example as CYERA_WEBHOOK_SECRET.
  3. For callbacks, create an API key with the Webhook Callback scope under Settings > API Keys > Create API Key.

If you receive Cyera webhooks through Hookdeck, create a source with the Cyera type and use its URL as the integration's endpoint. The source is for Cyera Privacy's privacy-request webhooks. Paste the Webhook Secret from the webhook integration into it. The source accepts POST only, which is the method Cyera uses.

Read the section on response codes below before you put Hookdeck between Cyera and your service, because Cyera reads meaning into the status code you return.

Response codes and callbacks

Cyera expects a 201 Created when a request has been created successfully, and a 200 OK for health checks and reminders. If a request has not been created, it expects a 4xx or 5xx, optionally with an error body such as {"error": "Invalid request signature"}.

Once the work is done, send a PUT to webhook_callback_url with your API key. The body repeats webhook_request_id and request_type, plus a status: completed or ignored for access and deletion, complete or error for opt-outs. Access callbacks can attach results, either as base64 files (200 MB in total) or as file paths in the cloud storage bucket integrated with Cyera. A readiness check callback sends an answer that matches one of the configured options. Cyera answers callbacks with 200, or 400, 404, 409 (already processed, or the readiness check has expired) or 422.

Securing Cyera webhooks

Read X-Webhook-Timestamp and X-Webhook-Signature. Build the signed bytes from the timestamp exactly as received, a colon, and the raw request body. Compute an HMAC-SHA256 of them with the Webhook Secret as a UTF-8 key, base64-encode the digest, and compare it with the header in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyCyeraSignature(rawBody, signatureHeader, timestampHeader, secret, toleranceSeconds = 0) {
  if (!signatureHeader || !timestampHeader || !secret) return false;

  // Optional replay window, off by default. Retries and replays resend the original timestamp.
  if (toleranceSeconds > 0) {
    if (!/^\d+$/.test(timestampHeader)) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > toleranceSeconds) return false;
  }

  // A colon, not a dot, between the timestamp and the body.
  const expected = crypto
    .createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(`${timestampHeader}:`, 'utf8'), rawBody]))
    .digest('base64');

  // Compare the base64 strings exactly: base64 is case-sensitive.
  const received = Buffer.from(signatureHeader.trim(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/cyera', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.CYERA_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const valid = verifyCyeraSignature(
    req.body,
    req.get('x-webhook-signature'),
    req.get('x-webhook-timestamp'),
    secret
  );
  if (!valid) return res.status(401).json({ error: 'Invalid request signature' });

  const request = JSON.parse(req.body.toString('utf8'));
  const isNew = typeof request.topic === 'string' && request.topic.endsWith(':create');
  // Record the request durably here, then do the work and PUT to request.webhook_callback_url.
  res.sendStatus(isNew ? 201 : 200);
});

The same check in Python:

import base64
import hashlib
import hmac
import time


def verify_cyera_signature(raw_body: bytes, signature_header, timestamp_header, secret: str,
                           tolerance_seconds: int = 0) -> bool:
    if not signature_header or not timestamp_header or not secret:
        return False
    # Optional replay window, off by default.
    if tolerance_seconds > 0:
        if not (timestamp_header.isascii() and timestamp_header.isdigit()):
            return False
        if abs(time.time() - int(timestamp_header)) > tolerance_seconds:
            return False
    signed = timestamp_header.encode("utf-8") + b":" + raw_body
    expected = base64.b64encode(
        hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
    )
    return hmac.compare_digest(signature_header.strip().encode("utf-8"), expected)


# In FastAPI: raw_body = await request.body()
# verify_cyera_signature(raw_body, request.headers.get("x-webhook-signature"),
#                        request.headers.get("x-webhook-timestamp"),
#                        os.environ["CYERA_WEBHOOK_SECRET"])

Cyera's examples in Shell, JavaScript, Python and Ruby all compute HMAC-SHA256(secret, "<timestamp>:<payload>") and base64-encode it, using a health check body. The separator is a colon. Code copied from a scheme that uses a dot, which is far more common, fails every time.

Cyera doesn't document a tolerance window for X-Webhook-Timestamp. The samples above leave the check off unless you pass a tolerance. Hookdeck applies no timestamp tolerance either, and its retries and replays resend the original header, so if you add a window, make it longer than any delay you allow for retries.

Make Cyera webhooks production-ready. Hookdeck Event Gateway verifies the base64 X-Webhook-Signature over the timestamp and raw body, stores every privacy request, and delivers it to your service with retries you control.

Cyera webhook limitations and pain points

Cyera reads meaning into the status code

The Problem: Cyera needs to know whether you created the request, and it learns that from the status code: 201 for created, 200 for health checks and reminders, 4xx or 5xx for not created.

Why It Happens: Cyera reads meaning into the response code, so the code your receiver returns is part of the protocol. Anything between Cyera and your code that answers on your behalf changes what Cyera sees. Cyera doesn't say whether it treats a 200 for a new request as success.

Workarounds:

  • If you receive Cyera's requests directly, return 201 only after the request is stored durably, and a 4xx or 5xx if it isn't.
  • If you put a queue or gateway in front, confirm with Cyera that a 200 is accepted for a new request before you rely on it.

How Hookdeck Can Help: Hookdeck answers Cyera with a 200 as soon as it has stored the request, so Cyera sees that rather than your service's 201 or error. In exchange, no request is lost while your service is down. Confirm with Cyera that it accepts the 200 for new requests, and treat the callback as the place where you report failures, for example with status: "error" on an opt-out.

The real outcome is a separate call

The Problem: Requests stay open in Cyera even though your system did the work.

Why It Happens: Completion is reported by your PUT to webhook_callback_url, not by the webhook response. A missing API key scope, a malformed body or a 409 because the request was already processed all leave the request unresolved.

Workarounds:

  • Store webhook_callback_url, webhook_request_id and the request ID with the job, and retry the callback on 5xx responses.
  • Treat a 409 as done, since Cyera uses it for requests already processed.

How Hookdeck Can Help: Hookdeck keeps every request with its full body, so the callback URL and IDs for any request are there to look up and replay if your job runner lost them.

Reminders arrive for incomplete requests

The Problem: The same privacy request appears more than once.

Why It Happens: Cyera sends privacy_request:reminder and opt_out:reminder for requests that are still incomplete, with the same privacy_request_id or opt_out_request_id.

Workarounds:

  • Key your jobs on privacy_request_id or opt_out_request_id, and treat a reminder as a prompt to check that job rather than to start a new one.
  • Answer reminders with 200, as Cyera asks.

How Hookdeck Can Help: Hookdeck filters can match on topic, so reminders can go to a monitoring destination while new requests go to the job runner.

Delivery behaviour is undocumented

The Problem: You don't know whether Cyera retries a request your endpoint missed.

Why It Happens: Cyera documents no retries, timeout or source IPs. Periodic health checks show whether your endpoint is up, but not what happens to a request sent during an outage.

Workarounds:

  • Keep the endpoint highly available and acknowledge quickly.
  • Watch the open requests in Cyera for any your systems never received.

How Hookdeck Can Help: Hookdeck accepts each request as it arrives and stores it, then delivers to your service with retries you configure, so an outage on your side doesn't depend on Cyera retrying.

Best practices

Verify against the raw body

The signature covers the exact bytes Cyera sent. In Express, use express.raw({ type: '*/*' }). In Next.js, use Buffer.from(await request.arrayBuffer()). In FastAPI, use await request.body().

Route on request_type, then topic

Answer health_check straight away. Start work only for :create topics, and use reminders to check on work already under way.

Store the request before you answer

Personal data requests have legal deadlines. Write the request, including webhook_callback_url, to durable storage before you respond, so nothing depends on an in-memory job.

Make handlers idempotent

Deduplicate on webhook_request_id for deliveries, and on privacy_request_id or opt_out_request_id for the work. See our guide to webhook idempotency and why to process webhooks asynchronously.

Conclusion

Cyera Privacy webhooks dispatch access, deletion and opt-out requests to your systems and expect a callback when the work is done. Verify X-Webhook-Signature as a base64 HMAC-SHA256 of the timestamp, a colon and the raw body, route on request_type and topic, and remember that the status code you return tells Cyera whether the request was created.

Hookdeck Event Gateway sits in front of your receiver, verifies every Cyera signature, and holds each privacy request until your service has processed it.

Get started with Hookdeck for free and handle Cyera webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.