# DatoCMS Webhooks

DatoCMS sends an HTTP POST when content or project state changes: a record is created, updated,
published or deleted, an upload is added, a build deploys. DatoCMS webhooks are **not signed**
and carry no timestamp. You can attach HTTP Basic credentials (`http_basic_user` /
`http_basic_password`) and custom headers to a webhook, and your endpoint authenticates the
request by checking the `Authorization: Basic` header against the credentials you configured.

> **A static shared credential authenticates the sender, not the body, and gives no replay
> protection** (no signature, no timestamp). Anyone who captures a request can replay it, and the
> credential says nothing about whether the payload was altered. Serve the endpoint over HTTPS
> only, keep the credentials secret, and rotate them if they leak.

## When to Use This Skill

* How do I receive DatoCMS webhooks?
* How do I verify DatoCMS webhook requests? (They aren't signed; use HTTP Basic auth)
* How do I check the Basic auth username and password DatoCMS sends?
* How do I handle `item` `publish` or `upload` `delete` events?
* Why does my DatoCMS webhook endpoint return 401?

## Verification (core)

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

// Hash both sides: equal-length digests, so timingSafeEqual never throws.
const digest = (v) => crypto.createHash('sha256').update(v).digest();
const safeEqual = (a, b) => crypto.timingSafeEqual(digest(a), digest(b));

function verifyDatoCmsRequest(headers, user = process.env.DATOCMS_WEBHOOK_USERNAME,
                              pass = process.env.DATOCMS_WEBHOOK_PASSWORD) {
  if (!user || !pass) return false;                         // never accept when unconfigured
  const m = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(headers['authorization'] || '');
  if (!m) return false;                                     // missing or malformed
  const decoded = Buffer.from(m[1], 'base64').toString('utf8');
  const i = decoded.indexOf(':');                           // split on the FIRST colon
  if (i < 0) return false;
  const userOk = safeEqual(decoded.slice(0, i), user);      // evaluate both: no short-circuit
  const passOk = safeEqual(decoded.slice(i + 1), pass);
  return userOk && passOk;
}

```

* Header: `Authorization: Basic base64(username:password)`. The scheme is case-insensitive and the
  password may itself contain colons, so split on the first colon only.
* Return `401` for a missing or malformed header or any mismatch, `400` for a body that isn't
  JSON, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse JSON
  only after the credential check. DatoCMS lets you send a non-JSON custom payload.
* Python: `hmac.compare_digest` on **bytes** (encode both sides; passing `str` raises on non-ASCII).

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

## Event Types

The default body has `entity_type`, `event_type`, `entity`, `environment` and more. A handler
dispatches on the pair.

| `entity_type` | `event_type` values | Sent When |
| --- | --- | --- |
| `item` | `create`, `update`, `delete`, `publish`, `unpublish` | A record changes state |
| `item_type` | `create`, `update`, `delete` | A model changes (including its fields) |
| `upload` | `create`, `update`, `delete` | A media asset changes |
| `build_trigger` | `deploy_started`, `deploy_succeeded`, `deploy_failed` | A build trigger deploys |
| `environment` | `deploy_started`, `deploy_succeeded`, `deploy_failed` | An environment deploys |
| `maintenance_mode` | `change` | An admin turns maintenance mode on or off |
| `sso_user` | `create` | An SSO user is added as a collaborator |
| `cda_cache_tags` | `invalidate` | CDA cache tags need invalidating |

Record updates also include `previous_entity`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/datocms-webhooks/references/overview.md)
for the payload and the draft/publish event sequences.

## Environment Variables

```bash
DATOCMS_WEBHOOK_USERNAME=your-basic-auth-username   # the webhook's HTTP basic auth user
DATOCMS_WEBHOOK_PASSWORD=your-basic-auth-password   # the webhook's HTTP basic auth password

```

## Local Development

DatoCMS can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 datocms --path /webhooks/datocms

```

Use the printed URL as the webhook URL, with the same username and password, in DatoCMS.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/datocms-webhooks/references/overview.md) - What DatoCMS webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/datocms-webhooks/references/setup.md) - Create the webhook and set Basic auth
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/datocms-webhooks/references/verification.md) - Credential check details, limits, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/datocms-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests