# Typefully Webhooks

## When to Use This Skill

* How do I receive Typefully webhooks?
* How do I verify the `X-Typefully-Signature` header (`sha256=<hex>`)?
* How do I handle `draft.published` or `draft.scheduled` events?
* Why is my Typefully webhook signature verification failing?
* Which events does Typefully send, and how does it retry?

## Essential Code (USE THIS)

Typefully (API v2) sends a JSON envelope `{ "event": "draft.created", "data": { ...draft } }` and signs each delivery with three headers:

```
X-Typefully-Event: draft.published
X-Typefully-Timestamp: 1737381605            (Unix seconds)
X-Typefully-Signature: sha256=<64 hex chars>

```

* Algorithm: HMAC-SHA256, lowercase hex, keyed with the webhook secret (from your Typefully API settings) used as-is, as UTF-8.
* Signed message: the `X-Typefully-Timestamp` value, a literal `.`, then the **raw request body** bytes.
* Typefully's docs only say to check that the timestamp is recent; they don't give a window. The examples leave it **off by default** (see below).

### Verification (Node.js)

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

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyTypefully(rawBody, timestamp, signature, secret) {
  if (!secret || !timestamp || !signature) return false;
  const digest = signature.startsWith('sha256=') ? signature.slice(7) : signature;
  if (!/^[0-9a-fA-F]{64}$/.test(digest)) return false; // validate before decoding
  const expected = crypto.createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]))
    .digest();
  const received = Buffer.from(digest.toLowerCase(), 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

```

### Verification (Python)

```python
import hashlib, hmac, re

def verify_typefully(raw_body: bytes, timestamp, signature, secret: str) -> bool:
    if not secret or not timestamp or not signature:
        return False
    digest = signature[len("sha256="):] if signature.startswith("sha256=") else signature
    if not re.fullmatch(r"[0-9a-fA-F]{64}", digest):
        return False
    message = b"".join([timestamp.encode("latin-1"), b".", raw_body])
    expected = hmac.new(secret.encode(), message, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(digest), expected)

```

Respond `401` when the signature or timestamp header is missing, malformed, or doesn't match. Respond with any `2xx` once the event is accepted.

**Replay window (opt-in):** Typefully suggests checking that the timestamp is recent. The examples don't, by default: Hookdeck's retries and replays resend the original timestamp, and Typefully doesn't document whether its own retries do, so a fixed window could reject legitimate redeliveries. Set `TYPEFULLY_WEBHOOK_TOLERANCE_SECONDS` to a positive number of seconds (the timestamp is Unix seconds) to opt in; unset or `0` leaves it off. Use idempotency on `data.id` plus the event for duplicate protection.

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

## Common Event Types

The type is in the body's `event` field (and the `X-Typefully-Event` header). The docs list seven:

| Event | Description |
| --- | --- |
| `draft.created` | A new draft is created |
| `draft.planned` | A draft is planned (dated but inert until confirmed) |
| `draft.scheduled` | A draft is scheduled for publishing |
| `draft.published` | A draft is successfully published |
| `draft.status_changed` | Any status transition (includes planned, scheduled, published) |
| `draft.tags_changed` | A draft's tags are modified |
| `draft.deleted` | A draft is deleted |

`data` holds the full draft (`id`, `social_set_id`, `status`, `scheduled_date`, `tags`, per-platform `platforms`, published URLs such as `x_published_url`). See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/typefully-webhooks/references/overview.md).

> **For the full event reference**, see [Typefully's API docs](https://typefully.com/docs/api).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Typefully-Signature` | `sha256=<hex>` HMAC-SHA256 signature |
| `X-Typefully-Timestamp` | Unix timestamp (seconds) when the event was generated |
| `X-Typefully-Event` | The event type, for example `draft.published` |

## Environment Variables

```bash
TYPEFULLY_WEBHOOK_SECRET=your_typefully_webhook_secret   # from Typefully's API settings
TYPEFULLY_WEBHOOK_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 typefully --path /webhooks/typefully

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/typefully-webhooks/references/overview.md) - Typefully webhook concepts, events, payloads, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/typefully-webhooks/references/setup.md) - Getting the secret and registering your endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/typefully-webhooks/references/verification.md) - Signature verification details and gotchas