# Semble Webhooks

## When to Use This Skill

* How do I receive Semble webhooks?
* How do I verify the `X-Webhook-Signature` header?
* How do I handle `booking.created`, `booking.updated`, `contact.created`, or `invoice.paid` events?
* Why is my Semble webhook signature verification failing?
* Why do Hookdeck retries fail Semble's timestamp check?

## Essential Code (USE THIS)

Every Semble delivery carries `X-Webhook-Signature: t=<unix seconds>,v1=<hex>`. `v1` is the lowercase hex HMAC-SHA256 of `"<t>.<raw body>"`, keyed with the subscription's `secret` as a UTF-8 string (the `createWebhook` GraphQL mutation returns it). Semble's docs show a single `v1` and don't describe sending several during rotation; the code below accepts the header if any `v1` matches, which is harmless. `X-Webhook-Timestamp` repeats `t`, but sign with the `t` from `X-Webhook-Signature`.

### Verification (Node.js)

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

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifySemble(rawBody, header, secret) {
  if (!secret || !header) return false;
  let t; const v1 = [];
  for (const part of header.split(',')) {            // trim: a space may follow the comma
    const kv = part.trim(), i = kv.indexOf('=');
    if (i <= 0) continue;
    const k = kv.slice(0, i).trim(), v = kv.slice(i + 1).trim();
    if (k === 't') t = v; else if (k === 'v1') v1.push(v);
  }
  if (!t || !/^\d+$/.test(t) || v1.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(rawBody).digest('hex');
  return v1.some((sig) => {
    if (!/^[0-9a-f]{64}$/i.test(sig)) return false;  // reject malformed values first
    const a = Buffer.from(sig.toLowerCase()), b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

```

### Verification (Python)

```python
import hashlib, hmac

def verify_semble(raw_body: bytes, header: str, secret: str) -> bool:
    if not secret or not header:
        return False
    t, v1 = None, []
    for part in header.split(","):
        k, sep, v = part.strip().partition("=")
        if sep and k.strip() == "t":
            t = v.strip()
        elif sep and k.strip() == "v1":
            v1.append(v.strip())
    if not t or not (t.isascii() and t.isdigit()) or not v1:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    # compare_digest on bytes: a non-ASCII header value fails the match instead of raising
    return any(hmac.compare_digest(sig.lower().encode(), expected.encode()) for sig in v1)

```

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

### Timestamp window: off by default

Semble's guide suggests rejecting timestamps older than 5 minutes. **Hookdeck retries and replays resend the original timestamp, so a 5-minute window rejects late retries.** The examples therefore skip the freshness check unless you set `SEMBLE_TIMESTAMP_TOLERANCE_SECONDS` to a positive number of seconds (default `0` = off). For idempotency, note that Semble documents `eventId` as unique per delivery attempt, so a Semble retry may carry a new one (a Hookdeck retry or replay resends the same body); make the handler idempotent on `eventType` plus the record IDs in `identifiers`. [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/semble-webhooks/references/verification.md) shows exactly where the window can be dropped or widened.

## Dispatch on `eventType`

The body is a JSON object: `subscriptionId`, `eventId`, `eventType`, `identifiers` (string IDs of related records), and optional `metadata`. `eventType` is a lowercase-first dotted camelCase string that matches what you subscribed to. Events don't overlap: subscribing to `booking.updated` does not deliver `booking.patientJourney.arrived`.

| Event | Triggered when | `identifiers` |
| --- | --- | --- |
| `booking.created` | An appointment was created | `bookingId`; optional `patientId`, `doctorId` |
| `booking.updated` | An appointment was updated | `bookingId`; optional `patientId`, `doctorId` |
| `booking.deleted` | An appointment was deleted | `bookingId`; optional `patientId`, `doctorId` |
| `contact.created` | A practice contact was created | `contactId` |
| `invoice.paid` | An invoice was fully paid | `invoiceId`, `paymentId`; optional `patientId` |
| `resourceAvailability.updated` | Appointment-system availability rules changed | `resourceAvailabilityId` |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/semble-webhooks/references/overview.md) for more events and the payload.

## Important Headers

| Header | Description |
| --- | --- |
| `X-Webhook-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>`; sign with this `t` |
| `X-Webhook-Timestamp` | `<unix seconds>`, the same value as `t` |

## Environment Variables

```bash
SEMBLE_WEBHOOK_SECRET=your_webhook_secret      # `secret` returned by createWebhook
SEMBLE_TIMESTAMP_TOLERANCE_SECONDS=0           # optional; a positive value opts in to the replay window

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/semble-webhooks/references/overview.md) - Semble webhook concepts, events, payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/semble-webhooks/references/setup.md) - Creating a subscription with the GraphQL API and saving the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/semble-webhooks/references/verification.md) - Signature details, the timestamp window, and debugging