[Developers](https://www.cv-transformer.com/nl/docs/developers) [Webhooks](https://www.cv-transformer.com/nl/docs/developers/webhooks)

# Webhooks

8 min lezen

Listen for events on your CV-Transformer account so your integration can automatically trigger real-time reactions.

## [Why use webhooks](#why-use-webhooks)

When building integrations with CV-Transformer, you might want your applications to receive events as they occur in your CV-Transformer accounts, so that your backend systems can execute actions accordingly. To enable webhook events, you need to register webhook endpoints. After you register them, CV-Transformer can push real-time event data to your application’s webhook endpoint when events happen in your CV-Transformer account. CV-Transformer uses HTTPS to send webhook events to your app as a JSON payload.

Receiving webhook events is particularly useful for listening to asynchronous events such as when a candidate is created, or when a member's permissions change.

## [Using webhooks](#using-webhooks)

When an event occurs, CV-Transformer generates a new Event object. For example, if you edit a layout, the `layouts.updated` event is emitted.

### [Event object](#event-object)

The Event object we send to your webhook endpoint provides a snapshot of the object that has been created or that has changed. The object has the following shape.

```
{
  "id": "00000000-0000-0000-0000-000000000000",
  "date": 1680064028,
  "event_type": "layouts.updated",
  "payload": {
    "new": {
      "id": "00000000-0000-0000-0000-000000000000",
      ...
    },
    "old": {
      "id": "00000000-0000-0000-0000-000000000000",
      ...
    }
  }
}
```

The `event_type` field in the Event object indicates the type of event that occurred. The `payload` field contains the new and old versions of the object that has been created or that has changed. The objects have the same fields as in the [API](https://www.cv-transformer.com/en/api-reference). For `.created` events, `old` is `null`.

### [Event types](#event-types)

CV-Transformer emits the following event types:

- `candidates.created` A candidate was created
- `candidates.updated` A candidate was updated
- `layouts.created` A layout was created
- `layouts.updated` A layout was updated
- `members.created` A member was added to the organization
- `members.updated` A member was updated, for example their permissions

Member events also contain the user's name and email under `user_profiles`, as `GET /v1/members?select=*,user_profiles(name,email)` returns them:

```
{
  "id": "00000000-0000-0000-0000-000000000000",
  "organization_id": "00000000-0000-0000-0000-000000000000",
  "user_id": "00000000-0000-0000-0000-000000000000",
  "permissions": ["edit_candidates", "view_candidates"],
  "user_profiles": { "name": "Jane Doe", "email": "jane@example.com" },
  ...
}
```

Removing a member or deleting a candidate or layout sends no event.

## [How to set up your webhook integration](#how-to-set-up-your-webhook-integration)

To start receiving webhook events in your app, follow the steps below. You can register one endpoint to handle several different event types at once, or set up individual endpoints for specific events.

1. Develop a webhook endpoint to receive events.
2. Register your endpoint in CV-Transformer under **Settings** → **Webhooks**, or with the [API](https://www.cv-transformer.com/en/api-reference).
3. Secure your webhook endpoint.

## [1. Create a webhook endpoint function](#1-create-a-webhook-endpoint-function)

Set up an HTTPS endpoint that can accept webhook requests with a POST method.

Set up your endpoint function so that it:

- Handles POST requests with a JSON payload consisting of an event object.
- Quickly returns a successful status code (`2xx`) prior to any complex logic that could cause a timeout.

### [Example endpoint](#example-endpoint)

This code snippet is a webhook function configured to check that the event type was received, to handle the event, and return a `200` response.

```
// This example uses Express to receive webhooks
const express = require("express");
const app = express();
// Match the raw body to content type application/json
// If you are using Express v4 - v4.16 you need to use body-parser, not express, to retrieve the request body
app.post(
  "/webhook",
  express.json({ type: "application/json" }),
  (request, response) => {
    const event = request.body;
    // Handle the event
    switch (event.event_type) {
      case "candidates.created":
        const candidate = event.payload.new;
        // Then define and call a method to handle the candidate creation.
        // handleCandidateCreated(candidate);
        break;
      case "layouts.updated":
        const layout = event.payload.new;
        // Then define and call a method to handle the layout update.
        // handleLayoutUpdated(layout);
        break;
      // ... handle other event types
      default:
        console.log(`Unhandled event type ${event.event_type}`);
    }
    // Return a response to acknowledge receipt of the event
    response.json({ received: true });
  }
);
app.listen(8000, () => console.log("Running on port 8000"));
```

## [2. Register and manage your webhook in CV-Transformer](#2-register-and-manage-your-webhook-in-cv-transformer)

After writing your webhook endpoint, register its URL under **Settings** → **Webhooks**, or with the [API](https://www.cv-transformer.com/en/api-reference), so that CV-Transformer knows where to deliver events. Provide the endpoint's publicly accessible HTTPS URL, and select the event types it receives.

### [Managing a webhook endpoint](#managing-a-webhook-endpoint)

You can update or delete webhook endpoints under **Settings** → **Webhooks**, or with the [API](https://www.cv-transformer.com/en/api-reference). Every delivery attempt is listed under **Settings** → **Logs** → **Webhooks**.

## [3. Secure your webhooks](#3-secure-your-webhooks)

After confirming that your webhook endpoint connection works as expected, secure the connection by implementing webhook best practices.

One especially important best practice is to use webhook signatures to verify that CV-Transformer generated a webhook request and that it didn’t come from a server acting like CV-Transformer.

### [Event ordering](#event-ordering)

CV-Transformer doesn’t guarantee delivery of events in the order in which they’re generated, so your endpoint shouldn’t rely on that order. You can also use the [API](https://www.cv-transformer.com/en/api-reference) to fetch any missing objects (for example, you can fetch the related layout using the information from `candidates.updated`).

### [Handle duplicate events](#handle-duplicate-events)

Webhook endpoints might occasionally receive the same event more than once. You can guard against duplicated event receipts by making your event processing idempotent. One way of doing this is logging the events you’ve processed, and then not processing already-logged events. The `webhook-id` header (the same as the `id` of the Event object) stays the same when we retry an event, so you can use it to recognize events you already processed.

### [Only listen to event types your integration requires](#only-listen-to-event-types-your-integration-requires)

Configure your webhook endpoints to receive only the types of events required by your integration. Listening for extra events (or all events) puts undue strain on your server and we don’t recommend it.

You can change the events that a webhook endpoint receives under **Settings** → **Webhooks**, or with the [API](https://www.cv-transformer.com/en/api-reference).

### [Verify events are sent from CV-Transformer](#verify-events-are-sent-from-cv-transformer)

Use webhook signatures to confirm that received events are sent from CV-Transformer. This allows you to verify that the events were sent by CV-Transformer, not by a third party. CV-Transformer signs every event in two ways:

- The `webhook-id`, `webhook-timestamp` and `webhook-signature` headers follow the [Standard Webhooks](https://www.standardwebhooks.com/) specification. They also protect against replay attacks, and you can verify them with any Standard Webhooks library. We recommend these for all integrations.
- The `X-Signature` header holds the signature that CV-Transformer has always sent. It is deprecated: it still works for existing integrations, but it uses MD5 and doesn't protect against replay attacks.

> **`X-Signature`** **is deprecated.** We still send it with every event, so existing integrations keep working, but we will stop sending it in the future and announce the date in advance. Verify the `webhook-signature` header instead, as described below.

Each event is sent with these headers:

- `webhook-id`: the id of the event. It stays the same when an event is retried.
- `webhook-timestamp`: when this delivery attempt was signed, in seconds since 1 January 1970 (UTC).
- `webhook-signature`: `v1,` followed by the base64-encoded HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`, with your endpoint’s secret as the key.
- `X-Signature` (deprecated): `\x` followed by the hex-encoded MD5 hash of `{secret}.{body}`.

To verify a signature, retrieve your endpoint’s secret, then verify the signature as described below.

#### [Retrieving your endpoint’s secret](#retrieving-your-endpoint-s-secret)

CV-Transformer generates a unique secret key for each endpoint. To retrieve the secret, go to **Settings** → **Webhooks** and copy the secret below the endpoint URL. You can also use the [API](https://www.cv-transformer.com/en/api-reference) to retrieve the secret. Additionally, if you use multiple endpoints, you must obtain a secret for each one you want to verify signatures on.

#### [Verifying the signature](#verifying-the-signature)

Both signatures are computed over the request body exactly as CV-Transformer sent it. Verify them against the raw body, before parsing it: parsing the JSON and stringifying it again changes its formatting, and with it the signature.

To verify the Standard Webhooks signature, perform the following steps:

1. Prepare the signed content by joining the `webhook-id` header, the `webhook-timestamp` header and the raw request body, separated by the character `.`.
2. Compute the expected signature: the HMAC-SHA256 of the signed content, with your endpoint’s secret as the key, encoded as base64.
3. The `webhook-signature` header holds a space-separated list of signatures, each prefixed with a version (`v1,`). If one of the `v1` signatures matches the expected signature, the event is verified. Compare them with a constant-time comparison.
4. Check that the `webhook-timestamp` is recent (see "Preventing replay attacks" below).

Standard Webhooks libraries expect the secret in base64. Pass your endpoint’s secret base64-encoded, for example `new Webhook(Buffer.from(signingSecret).toString("base64"))` with the `standardwebhooks` package for JavaScript.

If your integration still verifies the deprecated `X-Signature` header, it does so as follows. Move it to the `webhook-signature` header above.

1. Prepare the signed payload by concatenating your endpoint’s secret with the character `.` and then the raw request body.
2. Compute the expected signature by hashing the signed payload with the MD5 hash function, encoded as hex.
3. Compare `\x` followed by the expected signature to the `X-Signature` header. If they match, the event is verified.

#### [Example of verifying signatures manually](#example-of-verifying-signatures-manually)

```
// Set your secret key.
const signingSecret = "...";
// Make sure that the secret is never exposed to the client
// This example uses Express to receive webhooks
const express = require("express");
const crypto = require("crypto");
const app = express();
const isEqual = (a, b) =>
  a.length === b.length &&
  crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
app.post(
  "/webhook",
  // Keep the raw body: the signatures are computed over it
  express.raw({ type: "application/json" }),
  (request, response) => {
    const body = request.body.toString("utf8");
    // Standard Webhooks
    const id = request.headers["webhook-id"];
    const timestamp = request.headers["webhook-timestamp"];
    const expectedSignature = crypto
      .createHmac("sha256", signingSecret)
      .update(`${id}.${timestamp}.${body}`)
      .digest("base64");
    const isSigned = (request.headers["webhook-signature"] ?? "")
      .split(" ")
      .some((signature) => isEqual(signature, `v1,${expectedSignature}`));
    const isRecent =
      Math.abs(Date.now() / 1000 - Number(timestamp)) <= 5 * 60;
    if (!isSigned || !isRecent) return response.status(400).end();
    // Or, with the deprecated X-Signature header:
    // const expectedSignature = crypto
    //   .createHash("md5")
    //   .update(`${signingSecret}.${body}`)
    //   .digest("hex");
    // if (!isEqual(request.headers["x-signature"] ?? "", `\x${expectedSignature}`))
    //   return response.status(400).end();
    const event = JSON.parse(body);
    // Handle the event
    response.json({ received: true });
  }
);
app.listen(8000, () => console.log("Running on port 8000"));
```

#### [Preventing replay attacks](#preventing-replay-attacks)

A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, CV-Transformer includes a timestamp in the `webhook-timestamp` header. Because this timestamp is part of the signed content, it’s also verified by the `webhook-signature`, so an attacker can’t change the timestamp without invalidating the signature.

If the signature is valid but the timestamp is too old, reject the payload. We suggest tolerating at most 5 minutes between the timestamp and the current time.

CV-Transformer generates the timestamp and signature each time it sends an event to your endpoint. When it retries an event, for example because your endpoint replied with a non-`2xx` status code, the new delivery attempt gets a new signature and timestamp.

The deprecated `X-Signature` header doesn’t sign a timestamp, so it doesn’t protect against replay attacks.

## [Retries](#retries)

A delivery counts as successful when your endpoint answers within 10 seconds with a `2xx` or `3xx` status code. Redirects are not followed.

When a delivery fails, we try again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours and 24 hours: 9 attempts over about three days.

If every attempt for an event fails and no other event reached the endpoint in the meantime, we disable the endpoint and email the members who can manage your organization's settings. A disabled endpoint receives no new events. Events that were still waiting for a retry are sent once you enable the endpoint again under **Settings** → **Webhooks**, or with the [API](https://www.cv-transformer.com/en/api-reference).

[VorigeEmbedding](https://www.cv-transformer.com/nl/docs/developers/embedding) [VolgendeMCP server](https://www.cv-transformer.com/nl/docs/developers/mcp-server)
