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:
{"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
- 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.
- 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. - 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.
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:
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 verifies the
X-Typefully-Signatureheader, 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
2xxas 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 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 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_atandX-Typefully-Timestamp, and skip repeats. - Make handlers idempotent, so applying the same draft state twice is harmless.
How Hookdeck Can Help: Hookdeck can deduplicate 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.
Acknowledge fast and process asynchronously
Verify, store the event, return 200, then do the work. See why to process webhooks asynchronously.
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 sits in front of your receiver, checks every Typefully signature, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Typefully webhooks reliably in minutes.