# Doppler Webhooks

## When to Use This Skill

* How do I receive Doppler webhooks?
* How do I verify the `X-Doppler-Signature` header?
* How do I handle `config.secrets.update` events (restart or redeploy when secrets change)?
* Why is my Doppler webhook signature verification failing?
* How do I verify a custom JSON payload from Doppler?

## Verification (core)

Doppler signing is optional: it only happens if you enter a **Secret** on the webhook, so always set one. The `X-Doppler-Signature` header is `sha256=<hex>`, the HMAC-SHA256 of the **raw request body** keyed on that Secret. Custom JSON payloads are signed the same way.

Do not copy the docs' Node sample, which hashes `JSON.stringify(req.body)`. Re-serialized JSON can differ from the bytes Doppler signed. Hash the raw bytes and compare timing-safe.

Node:

```javascript
const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

Python:

```python
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)

```

In Express use `express.raw({ type: '*/*' })` so the body stays a `Buffer` whatever the content type.

> **For complete handlers with route wiring, event dispatch, and tests**, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/doppler-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/doppler-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/doppler-webhooks/examples/fastapi/)

## Common Event Types

| Event (`type` field) | Description |
| --- | --- |
| `config.secrets.update` | Secrets changed in a config. The only documented type; the default payload always carries it |

The default payload has `type`, `config`, `project`, `workplace`, and `diff` (`added`, `removed`, `updated` secret names). A custom JSON payload replaces all of this and has no `type`, so don't assume one.

## Important Headers

| Header | Description |
| --- | --- |
| `X-Doppler-Signature` | `sha256=<hex>` HMAC-SHA256 of the raw body. Only sent when a Secret is set |

## Environment Variables

```bash
DOPPLER_WEBHOOK_SECRET=your_webhook_secret   # The "Secret" field on the Doppler webhook

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 doppler --path /webhooks/doppler

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/doppler-webhooks/references/overview.md) - Doppler webhook concepts and payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/doppler-webhooks/references/setup.md) - Configuration guide
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/doppler-webhooks/references/verification.md) - Signature verification details