> ## Documentation Index
> Fetch the complete documentation index at: https://developers.cutshort.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks — Configure Endpoints, Signatures, and Retries

> Programmatically register, list, and delete Cutshort webhook endpoints. Covers request body params, signature verification, and automatic retry behavior.

Use the webhooks API to programmatically manage your event endpoints. You can register multiple endpoints with different event subscriptions — for example, one endpoint for ATS sync and another for Slack notifications.

## List Webhooks

Retrieve all webhook endpoints registered for your organization.

```http theme={null}
GET https://api.cutshort.io/v1/webhooks
```

```bash cURL theme={null}
curl 'https://api.cutshort.io/v1/webhooks' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

```json Response theme={null}
{
  "data": [
    {
      "id": "wh_6kMpL3nR",
      "url": "https://yourapp.com/cutshort-events",
      "events": ["candidate.shortlisted", "invite.accepted"],
      "status": "active",
      "created_at": "2024-01-15T10:00:00Z"
    },
    {
      "id": "wh_9pNqR5sT",
      "url": "https://yourapp.com/slack-relay",
      "events": ["candidate.stage_changed", "offer.made"],
      "status": "active",
      "created_at": "2024-01-18T08:30:00Z"
    }
  ]
}
```

## Register a Webhook

Subscribe a new HTTPS endpoint to one or more event types.

```http theme={null}
POST https://api.cutshort.io/v1/webhooks
```

### Request Body

<ParamField body="url" type="string" required>
  The HTTPS URL that will receive webhook events. The endpoint must be publicly accessible and respond to POST requests.
</ParamField>

<ParamField body="events" type="array of strings" required>
  List of event types to subscribe to. See the [Webhook Events Reference](/api-reference/webhooks/events) for all valid values.
</ParamField>

<ParamField body="secret" type="string">
  A custom signing secret used for HMAC-SHA256 signature verification. If omitted, Cutshort generates a secret automatically and returns it in the response.
</ParamField>

### Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.cutshort.io/v1/webhooks' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "url": "https://yourapp.com/cutshort-events",
      "events": ["candidate.shortlisted", "invite.accepted"],
      "secret": "whsec_3xKpT9mNqL8vR2yD"
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.cutshort.io/v1/webhooks",
      json={
          "url": "https://yourapp.com/cutshort-events",
          "events": ["candidate.shortlisted", "invite.accepted"],
          "secret": "whsec_3xKpT9mNqL8vR2yD"
      },
      headers={
          "Authorization": "Bearer YOUR_API_KEY",
          "Content-Type": "application/json"
      }
  )
  webhook = response.json()
  print(f"Registered webhook: {webhook['id']}")
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "wh_6kMpL3nR",
  "url": "https://yourapp.com/cutshort-events",
  "events": ["candidate.shortlisted", "invite.accepted"],
  "secret": "whsec_3xKpT9mNqL8vR2yD",
  "status": "active",
  "created_at": "2024-01-15T10:00:00Z"
}
```

<Warning>
  Store the `secret` field immediately — it is only shown in full at creation time. You will need it to verify webhook signatures on every incoming request.
</Warning>

## Delete a Webhook

Permanently remove a registered webhook endpoint. Cutshort will stop delivering events to it immediately.

```http theme={null}
DELETE https://api.cutshort.io/v1/webhooks/{id}
```

```bash cURL theme={null}
curl -X DELETE 'https://api.cutshort.io/v1/webhooks/wh_6kMpL3nR' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

Returns `204 No Content` on success with an empty response body.

## Verifying Signatures

Every webhook request Cutshort sends includes an `X-Cutshort-Signature` header:

```
X-Cutshort-Signature: sha256=<hash>
```

To verify a request is genuine, compute the HMAC-SHA256 of the **raw request body** using your webhook secret, then compare it to the hash in the header. Always use a constant-time comparison to prevent timing attacks.

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  import json
  from fastapi import FastAPI, Request, HTTPException

  app = FastAPI()
  WEBHOOK_SECRET = "whsec_3xKpT9mNqL8vR2yD"

  def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(f"sha256={expected}", signature)

  @app.post("/cutshort-events")
  async def handle_webhook(request: Request):
      payload = await request.body()
      sig = request.headers.get("X-Cutshort-Signature", "")
      if not verify_webhook(payload, sig, WEBHOOK_SECRET):
          raise HTTPException(status_code=401, detail="Invalid signature")
      event = json.loads(payload)
      # process event...
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');
  const express = require('express');

  const app = express();

  function verifyWebhook(payload, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
    return crypto.timingSafeEqual(
      Buffer.from(`sha256=${expected}`),
      Buffer.from(signature)
    );
  }

  app.post(
    '/cutshort-events',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const sig = req.headers['x-cutshort-signature'];
      if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }
      const event = JSON.parse(req.body);
      // process event...
      res.sendStatus(200);
    }
  );
  ```
</CodeGroup>

## Retry Behavior

<Note>
  When a webhook delivery fails — either a non-2xx response or no response within **10 seconds** — Cutshort retries the delivery up to **5 times** on an exponential schedule:

  | Attempt   | Delay after previous failure |
  | --------- | ---------------------------- |
  | 1st retry | 1 minute                     |
  | 2nd retry | 5 minutes                    |
  | 3rd retry | 30 minutes                   |
  | 4th retry | 2 hours                      |
  | 5th retry | 8 hours                      |

  After 5 consecutive failures the endpoint is automatically **disabled**. To re-enable it, either use your Cutshort dashboard or send a PATCH request to `/v1/webhooks/{id}` with `"status": "active"`.
</Note>
