# Guide to Typefully Webhooks: Features and Best Practices

Typefully webhooks tell your systems what happens to drafts: one is created, scheduled, published or deleted, its status changes, or its tags are edited. Typefully is a tool for writing and scheduling posts on X, LinkedIn, Threads, Bluesky, Mastodon and Substack, and webhooks let you react to its publishing workflow, for example by logging published posts or notifying a team when a draft is scheduled.

This guide covers the events Typefully sends, how deliveries are signed, how Typefully retries and disables webhooks, and the practices that keep a Typefully receiver reliable.

## What are Typefully webhooks?

Typefully's API v2 supports webhooks that send a JSON payload to your endpoint when a draft event occurs. You subscribe to any combination of seven draft events. Each payload has two fields: `event`, the event type, and `data`, the full draft as the API's `DraftDetailResponse` describes it.

Every request carries `X-Typefully-Signature` in the form `sha256=<hex>`. The hex is an HMAC-SHA256 of the `X-Typefully-Timestamp` header, a `.`, and the raw body, keyed with the webhook secret.

## Typefully webhook features

| Feature | Details |
| --- | --- |
| Configuration | Typefully's API settings (where its docs say to re-enable a disabled webhook) |
| Method | `POST` with a JSON body |
| Events | Seven draft events, in any combination |
| Signature header | `X-Typefully-Signature`, `sha256=<hex>` |
| Timestamp header | `X-Typefully-Timestamp`, a Unix timestamp of when the event was generated |
| Event header | `X-Typefully-Event` |
| Scheme | HMAC-SHA256 |
| Signed content | `<timestamp>.<raw body>` |
| Payload | `{ event, data }`, compact JSON with sorted keys |
| Acknowledgement | Any `2xx` |
| Retries | Up to 4 retries (5 attempts) with exponential backoff over 1 hour |
| Auto-disable | After 100 consecutive failures, with an email |
| Response timeout | Not documented |

## Common events

| Event | Description |
| --- | --- |
| `draft.created` | A new draft is created |
| `draft.planned` | A draft is planned: it has a date but won't publish until confirmed |
| `draft.scheduled` | A draft is scheduled for publishing |
| `draft.published` | A draft is published successfully |
| `draft.status_changed` | Any status transition, including planned, scheduled and published |
| `draft.tags_changed` | A draft's tags are modified |
| `draft.deleted` | A draft is deleted |

Typefully says `draft.status_changed` fires on any status transition, including planned, scheduled and published. A webhook subscribed to it and to `draft.planned`, `draft.scheduled` or `draft.published` can expect two deliveries for one transition, so subscribe to one or the other.

`data` holds the draft at the time of the event. Its required fields are `id`, `social_set_id`, `draft_id`, `status`, `created_at`, `preview`, `private_url` and `platforms`. It also carries `scheduled_date`, `published_at`, `tags` (slugs, not names), `draft_title`, `share_url`, and a published URL and time for each platform, such as `x_published_url`. A delivery looks like this, with `data` trimmed:

```json
{"data":{"id":12345,"private_url":"https://typefully.com/?d=12345&a=67890","published_at":"2025-01-20T14:00:05Z","scheduled_date":"2025-01-20T14:00:00Z","social_set_id":67890,"status":"published","tags":["marketing","product"],"x_published_url":"https://x.com/username/status/1234567890"},"event":"draft.published"}

```

The field values come from the examples in Typefully's API schema. Route on `event`, which also arrives in the `X-Typefully-Event` header.

## Setting up Typefully webhooks

1. Add a webhook in Typefully with your receiver's URL and the events you want. Typefully's API reference doesn't describe the setup screens; the only place it names is your API settings, where you re-enable a disabled webhook.
2. Copy the webhook secret and store it, for example as `TYPEFULLY_WEBHOOK_SECRET`. The API reference uses the secret in its sample but doesn't say where it's shown.
3. Trigger an event, for example by creating a draft, and check that your receiver verifies it.

**With Hookdeck.** Typefully is a supported source type in Hookdeck. Create a Hookdeck source of the Typefully type, paste the webhook secret, and use the Hookdeck source URL as the webhook URL in Typefully. The source accepts `POST` only and checks the hex after `sha256=` in `X-Typefully-Signature` against `<X-Typefully-Timestamp>.<raw body>`. A request that fails the check still gets a `200` from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

## Securing Typefully webhooks

Join the `X-Typefully-Timestamp` header, a literal `.`, and the raw body bytes. Compute an HMAC-SHA256 with the secret as a UTF-8 string, then compare it in constant time with the hex that follows `sha256=` in `X-Typefully-Signature`.

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

function verifyTypefullySignature(rawBody, header, timestamp, secret) {
  if (!header || !timestamp || !secret) return false;
  const match = /^sha256=([0-9a-f]{64})$/i.exec(header); // validate before decoding
  if (!match) return false;

  const signed = Buffer.concat([Buffer.from(`${timestamp}.`, 'latin1'), rawBody]);
  const expected = crypto.createHmac('sha256', secret).update(signed).digest();
  const received = Buffer.from(match[1], 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

// Optional replay check, off by default (0). Hookdeck retries and replays resend the original timestamp.
const TOLERANCE_SECONDS = Number(process.env.TYPEFULLY_WEBHOOK_TOLERANCE_SECONDS || 0);

const app = express();

app.post('/webhooks/typefully', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.TYPEFULLY_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const timestamp = req.get('x-typefully-timestamp');
  if (!verifyTypefullySignature(req.body, req.get('x-typefully-signature'), timestamp, secret)) {
    return res.sendStatus(401);
  }
  if (TOLERANCE_SECONDS > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.event, then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac
import re

SIGNATURE = re.compile(r"sha256=([0-9a-fA-F]{64})")

def verify_typefully_signature(raw_body: bytes, header, timestamp, secret: str) -> bool:
    if not header or not timestamp or not secret:
        return False
    match = SIGNATURE.fullmatch(header)
    if not match:
        return False
    signed = timestamp.encode("latin-1") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(match.group(1)), expected)

```

Typefully's own Python sample decodes the body as UTF-8, rebuilds the string and compares the whole `sha256=<hex>` value. That gives the same result for a UTF-8 body. Working on the raw bytes, as above, avoids any re-encoding.

Typefully says to "verify the timestamp is recent to prevent replay attacks" but doesn't give a window. The samples leave the check off by default because Hookdeck applies no timestamp tolerance and its retries and replays resend the original headers. Typefully also describes the timestamp as the time the event was generated, not the time of the attempt, so a tight window could reject Typefully's own later retries too. If you turn the check on, choose a window longer than the retry and replay periods you rely on.

> **Make Typefully webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-Typefully-Signature` header, accepts every draft event while your service is down, and keeps your webhook clear of Typefully's auto-disable rule.

## Typefully webhook limitations and pain points

### A one-hour retry window

**The Problem:** An outage of more than an hour loses draft events.

**Why It Happens:** Typefully retries a failed delivery up to 4 times with exponential backoff over a 1-hour window, then stops.

**Workarounds:**

* After an outage, list drafts through the API and reconcile them with your records.
* Return `2xx` as soon as the signature checks out, and keep the handler's work small.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as it arrives and stores it, then delivers to your service with [automatic retries](/docs/retries) over a window you choose, so a long outage doesn't drop events.

### Webhooks are disabled after 100 failures in a row

**The Problem:** A broken endpoint stops receiving events altogether.

**Why It Happens:** Typefully disables a webhook after 100 consecutive failures and emails you. It stays off until you re-enable it from your API settings.

**Workarounds:**

* Watch for the email and alert on failures in your own logs.
* Re-enable the webhook as soon as the endpoint is fixed, then reconcile through the API.

**How Hookdeck Can Help:** Hookdeck keeps answering Typefully while your service is down, so the webhook doesn't reach the failure threshold, and you can [replay](/docs/use-cases/receive-webhooks#replay-for-debugging-and-recovery) the stored events once your service recovers.

### No event ID

**The Problem:** You can't tell a retry from a new event with a single key.

**Why It Happens:** The payload has `event` and the draft in `data`, but no delivery or event identifier.

**Workarounds:**

* Build a key from `event`, `data.id`, `data.updated_at` and `X-Typefully-Timestamp`, and skip repeats.
* Make handlers idempotent, so applying the same draft state twice is harmless.

**How Hookdeck Can Help:** Hookdeck can [deduplicate](/docs/deduplication) requests on fields you choose before they reach your service.

### Sparse setup documentation

**The Problem:** It's unclear where to find the secret or how long Typefully waits for a response.

**Why It Happens:** Typefully's API reference covers events, payloads, signing and retries, but not the setup screens, the secret's location or a response timeout.

**Workarounds:**

* Look in your API settings for the webhook and its secret.
* Respond quickly, since the timeout isn't published.

**How Hookdeck Can Help:** Hookdeck answers Typefully immediately and records every request with its headers, so you can see exactly what Typefully sent.

## Best practices

### Verify against the raw body

Typefully serializes the payload with compact separators and sorted keys, and the signature covers those exact bytes. Use `express.raw()` in Express, `await request.arrayBuffer()` in a Next.js route handler, and `await request.body()` in FastAPI, rather than re-serializing parsed JSON.

### Handle the `sha256=` prefix

The header value is `sha256=<hex>`. Strip the prefix before decoding the hex, or compare the whole string, but don't compare raw hex with the prefixed value.

### Pick specific events or `draft.status_changed`

Subscribing to both duplicates work for each status change. Choose the specific events if you only care about publishing, or `draft.status_changed` if you track every transition.

### Deduplicate

Use a key built from the event and the draft, and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Verify, store the event, return `200`, then do the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Typefully signs every webhook with `X-Typefully-Signature`, `sha256=` followed by a hex HMAC-SHA256 of `<timestamp>.<raw body>`. Verify against the raw bytes, strip the prefix before decoding, choose a replay window with retries and replays in mind, and answer quickly so the webhook stays enabled.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks every Typefully signature, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Typefully webhooks reliably in minutes.