# Webhooks

> Get a signed HTTPS request at your own endpoint each time a candidate applies, and verify it, test it, and handle retries.

## Overview

Webhooks send an HTTPS request from Dover to a URL you choose when something happens. Today there is one event, **Candidate applied**, which fires each time someone applies to one of your jobs. Use it to update your own systems, post to a chat tool, or start a workflow, without checking Dover for new applicants. Adding and managing endpoints needs the **Integrations** company permission.

---

## Add an endpoint

1. Click **Settings → Integrations**.
2. Open **Webhooks**.
3. Under **Add new endpoint**, enter your **Endpoint URL**. It must be public and use HTTPS.
4. Choose the **Event**, **Candidate applied**.
5. Click **Add endpoint**.
6. In **Save your signing secret**, click **Copy signing secret** and store it somewhere safe.

:::caution
Dover shows the signing secret only once. If you lose it, delete the endpoint and add it again to get a new one.
:::

---

## Manage your endpoints

**Active endpoints** lists each endpoint with its **Event** and **Status**.

- **Pause or resume:** use the switch to set an endpoint **Active** or **Paused**. A paused endpoint gets nothing, and requests waiting to be sent to it are cancelled.
- **Test:** click **Test** to send a sample event to an active endpoint. Dover shows **Test event queued**.
- **Delete:** click the delete icon and confirm.

---

## What Dover sends

Each request is a `POST` with a JSON body. The event type is `candidate.application.created`.

```json
{
  "id": "6f1f6c2e-...",
  "type": "candidate.application.created",
  "api_version": "2026-07-23",
  "created_at": "2026-11-02T15:04:05+00:00",
  "data": {
    "client": { "id": "..." },
    "job": { "id": "...", "title": "Senior Software Engineer" },
    "application": {
      "id": "...",
      "applied_at": "2026-11-02T15:03:58+00:00",
      "external_application_id": null,
      "source": { "id": "...", "label": "Dover careers page" },
      "candidate": {
        "first_name": "Alice",
        "last_name": "Johnson",
        "full_name": "Alice Johnson",
        "email": "alice@example.com",
        "linkedin_public_id": null
      },
      "application_questions": [{ "question": "Why this role?", "answer": "..." }]
    }
  }
}
```

A test event has the type `webhook.test` and only the company's ID in `data`.

Every request has these headers:

| Header | What it holds |
| --- | --- |
| `Dover-Webhook-Id` | The event's ID. The same event has the same ID on every retry. |
| `Dover-Webhook-Delivery-Id` | The ID of this delivery. |
| `Dover-Webhook-Timestamp` | When Dover signed the request, in Unix seconds. |
| `Dover-Webhook-Version` | The payload version, such as `2026-07-23`. |
| `Dover-Webhook-Signature` | The signature, as `v1=` followed by a hex value. |
| `Idempotency-Key` | The event's ID, so you can ignore repeats. |

---

## Verify a request

Check the signature before you trust a request.

1. Read the raw request body, exactly as received.
2. Join the `Dover-Webhook-Timestamp` value, a period, and the raw body.
3. Compute an HMAC-SHA256 of that text, using your signing secret as the key, and write it as hex.
4. Compare it to the value after `v1=` in `Dover-Webhook-Signature`, using a constant-time comparison.

```python
import hashlib, hmac

def is_valid(secret: str, timestamp: str, body: bytes, header: str) -> bool:
    signed = timestamp.encode() + b"." + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"v1={expected}", header)
```

Reject requests with an old timestamp to block replays. Use `Dover-Webhook-Id` to ignore an event you already handled.

---

## Delivery and retries

Respond with a `2xx` status within 10 seconds. If Dover gets a timeout, a network error, a `408`, `425`, `429`, or any `5xx`, it tries again about 5 minutes later, up to 10 attempts in total. Other responses, such as `400` or `404`, aren't retried. Dover doesn't follow redirects.

---

## Frequently asked questions

### Why can't I add my URL?

Dover accepts only public HTTPS addresses. It rejects localhost, private network addresses, URLs with a username or password, and hosts it can't resolve.

### I lost my signing secret. What do I do?

Delete the endpoint and add it again. The new endpoint gets a new secret, shown once.

### Why did I get the same event twice?

Dover retries when it doesn't get a `2xx` answer in time. Use `Dover-Webhook-Id` or `Idempotency-Key` to ignore an event you already handled.

### Why is the Test button turned off?

You can only test an **Active** endpoint. Switch it on first.

### Which events can I get?

Only **Candidate applied** for now.
