# Plain Webhooks

## When to Use This Skill

* How do I receive Plain webhooks?
* How do I verify the `Plain-Request-Signature` header?
* Why is my Plain webhook signature verification failing (or why are requests unsigned)?
* How do I handle `thread.thread_created`, `thread.email_received`, or `customer.customer_created` events?

## Verification (core)

Plain signs the **raw request body** with HMAC-SHA256, keyed with your workspace HMAC secret, and sends the lowercase hex digest in the `Plain-Request-Signature` header. Requests are only signed once a workspace HMAC secret exists; without one the header is absent. Plain's docs snippet hashes `JSON.stringify(request.body)`, but the official `@team-plain/webhooks` SDK hashes the raw payload, so use the raw body (re-serialized JSON can differ byte for byte).

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

function verifyPlainSignature(rawBody, signature, secret) {
  // Exactly 64 hex chars, checked before any hex decoding (invalid hex silently truncates)
  if (!signature || !/^[0-9a-f]{64}$/i.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature.toLowerCase(), 'utf8'),
    Buffer.from(expected, 'utf8')
  );
}

```

In Python, use `hmac.compare_digest` on bytes (`expected.encode()` and `signature.lower().encode()`) so a non-ASCII header gives a 401, not a 500.

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

## Setup (with or without Hookdeck)

1. In Plain, go to **Settings → Request signing** (workspace admins only) and generate/copy the HMAC secret.
2. Create a webhook target in **Settings → Webhooks → Add webhook target**.
3. Store the secret as `PLAIN_WEBHOOK_SECRET`. If you use Hookdeck, turn on request signing in Plain and paste its HMAC secret into the source's verification settings. Without request signing, requests are unsigned and fail verification.

Express handlers must read the body with `express.raw({ type: '*/*' })` so any content type verifies. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/plain-webhooks/references/setup.md).

## Common Event Types

The body is `{id, type, timestamp, workspaceId, payload: {eventType, ...}, webhookMetadata: {...}}`. Dispatch on `payload.eventType` (also sent as the `Plain-Event-Type` header).

| `payload.eventType` | Triggered When |
| --- | --- |
| `thread.thread_created` | A thread is created, on any channel |
| `thread.email_received` | An email arrives on a thread |
| `thread.thread_status_transitioned` | A thread changes status |
| `customer.customer_created` | A customer is created |

> **For the full event reference**, see [Plain webhook events](https://www.plain.com/docs/webhooks/thread-created).

## Environment Variables

```bash
PLAIN_WEBHOOK_SECRET=your_plain_hmac_secret   # Settings > Request signing
PLAIN_TOLERANCE_SECONDS=300                   # Optional replay check; off when unset

```

## Local Development

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

```

No account required; the CLI creates a guest account on first run.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/plain-webhooks/references/overview.md) - Plain webhook concepts, payload shape, events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/plain-webhooks/references/setup.md) - Request signing, webhook targets, Hookdeck
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/plain-webhooks/references/verification.md) - Signature details and gotchas