# PlanetScale Webhooks

## When to Use This Skill

* How do I receive PlanetScale webhooks?
* How do I verify the `X-PlanetScale-Signature` header?
* How do I handle `branch.ready`, `deploy_request.opened`, `backup.failed` or `cluster.storage` events (notify Slack, update a ticket, run CI on schema changes)?
* Why is my PlanetScale webhook signature verification failing (the docs' JavaScript sample uses `JSON.stringify(req.body)`)?

## Verification (core)

PlanetScale signs the **raw request body** with HMAC-SHA256, keyed with the webhook's secret, and sends the **bare lowercase hex digest** in `X-PlanetScale-Signature`. There is no `sha256=` prefix, no timestamp and no list of signatures: compare the whole header value as one digest.

* The HMAC key is the secret's UTF-8 bytes, used as-is (not hex- or base64-decoded).
* Sign **bytes**: the body exactly as received. Never `JSON.parse` / `JSON.stringify` before verifying (the docs' JavaScript sample does `JSON.stringify(req.body)`, which does not match byte-for-byte; the Python sample uses the raw body, and so should you).
* Require exactly 64 hex characters before decoding (`Buffer.from(x, 'hex')` silently drops bad characters), then compare in constant time. Hex is case-insensitive.

Node:

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

function verify(rawBody, signatureHeader, secret) {
  if (typeof signatureHeader !== 'string' || !secret) return false;
  if (!/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(signatureHeader, 'hex'), expected);
}

```

Python (compare bytes so a non-ASCII header returns 401, not a 500):

```python
import hmac, hashlib, re

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    if not re.fullmatch(r"[0-9a-fA-F]{64}", signature_header):
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header.lower().encode("utf-8"), expected.encode("utf-8"))

```

Missing, empty or malformed header, or a mismatch: respond `401`. 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/planetscale-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/planetscale-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/planetscale-webhooks/examples/fastapi/)

## Common Event Types

The event name is the body's top-level `event` field (there is no event-name header). `timestamp` (Unix seconds), `organization`, `database` and `resource` (the resource's API representation) sit beside it.

| Event | Fires when |
| --- | --- |
| `webhook.test` | You click **Test webhook** (one test every 20 seconds) |
| `branch.ready` | A branch is created and ready to connect |
| `branch.anomaly` | A branch has a new anomaly in Insights |
| `deploy_request.opened` | A deploy request is opened |
| `deploy_request.queued` | A deploy request is added to the deploy queue |
| `deploy_request.in_progress` | A deploy request has started running |
| `backup.failed` | A backup failed |
| `backup.succeeded` | A backup completed |
| `cluster.storage` | A Postgres database reached a storage threshold (60/75/85/90/95%) |
| `database.access_request` | PlanetScale staff opens an access request |

PlanetScale documents 21 events in total, including `branch.primary_promoted`, `branch.out_of_memory`, `branch.schema_recommendation`, `branch.sleeping`, `branch.start_maintenance`, `deploy_request.pending_cutover`, `deploy_request.schema_applied`, `deploy_request.errored`, `deploy_request.reverted`, `deploy_request.closed` and `keyspace.storage`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/planetscale-webhooks/references/overview.md).

> **For the full event reference**, see [PlanetScale webhook events](https://planetscale.com/docs/api/webhook-events).

## Important Headers

| Header | Description |
| --- | --- |
| `X-PlanetScale-Signature` | Hex HMAC-SHA256 of the raw body, keyed with the webhook secret |
| `Authorization` | Only if you configured one on the webhook; sent unchanged. Authenticates PlanetScale to you and does not replace the signature |

## Delivery Behaviour

Respond `2xx` within **2 seconds** or the delivery is marked failed; redirects are not followed; webhooks that repeatedly fail are disabled. No retry policy is documented, so do the work asynchronously and dedupe on your side (there is no delivery ID header; derive a key from `event`, `resource.id` and `timestamp`). Each database can have up to 5 webhooks. Deliveries come from `3.209.149.66`, `3.215.97.46` and `34.193.111.15`.

## Environment Variables

```bash
PLANETSCALE_WEBHOOK_SECRET=your_webhook_secret   # Database Settings > Webhooks > ... > Show secret

```

## Setup

In the dashboard open your database, then **Settings → Webhooks → Add webhook**, enter an `https://` URL, pick events and save. Use **… → Show secret** to copy the secret (one per webhook), and **… → Test webhook** to send `webhook.test`. Webhooks can also be managed with `pscale webhook` or the API. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/planetscale-webhooks/references/setup.md).

When using Hookdeck, paste the webhook secret into the PlanetScale source's webhook secret field: Hookdeck's source verifies `X-PlanetScale-Signature` with the same HMAC-SHA256 hex scheme.

## Local Development

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

```

## Reference Materials

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