# Basin Webhooks

## When to Use This Skill

* How do I receive Basin webhooks?
* How do I verify the `X-Basin-Signature` header on a Basin webhook?
* How do I handle a Basin form submission webhook (JSON or form-urlencoded)?
* Why is my Basin webhook signature verification failing?
* Why are my Basin webhooks arriving without a signature?

## Verification (core)

Basin sends the header `X-Basin-Signature: sha256=<hex>`, where `<hex>` is the lowercase hex
**HMAC-SHA256 of the raw request body**, keyed with the webhook signing secret. There is no
timestamp and it is not the Standard Webhooks spec. Require and strip the `sha256=` prefix, then
compare in constant time. Verify over the **raw bytes before parsing**: the body is JSON or
form-urlencoded depending on the webhook's payload type.

Node (check the hex shape first: `Buffer.from(x, 'hex')` silently truncates invalid hex):

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

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

```

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

```python
import hashlib, hmac

def verify_basin_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    if not signature_header or not signature_header.startswith("sha256="):
        return False
    received = signature_header[len("sha256="):].lower().encode("utf-8")
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest().encode("utf-8")
    return hmac.compare_digest(received, expected)

```

> **Plan requirement**: signed webhooks are only available on Basin's **Growth plan and above**.
> On lower plans deliveries carry **no** `X-Basin-Signature` header, so verification fails.
> Express handlers must read the body with `express.raw({ type: '*/*' })` so any content type verifies.

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

## Events

Basin has **no event types**. Each webhook fires once per form submission (non-spam by default;
there is a checkbox to also fire for spam). Failed deliveries are retried up to 15 times over
24-28 hours with exponential backoff, and can be re-fired from the submission log or API.

## Payload Types

The format is chosen per webhook and mirrors the original submission:

| Type | Body |
| --- | --- |
| JSON | `{"email": "...", "name": "...", "id": "submission_id", "attachments": [...]}` |
| Data wrapped JSON | `{"data": {"email": "...", "id": "..."}}` |
| Form URL encoded | `email=...&name=...&id=...` |

Use the submission `id` as your idempotency key.

## Environment Variables

```bash
BASIN_WEBHOOK_SECRET=your_signing_secret   # Form dashboard > Integrations > Webhooks > Edit > Webhook Signing > Show

```

## Setup with Hookdeck

Basin signs webhooks only on the Growth plan and above: paste the webhook signing secret into the
Hookdeck source's webhook verification settings. On lower plans, deliveries are unsigned and fail
verification.

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/basin-webhooks/references/overview.md) - Basin webhook concepts and payload types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/basin-webhooks/references/setup.md) - Configure a webhook and find the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/basin-webhooks/references/verification.md) - Signature verification details and gotchas