Gareth Wilson Gareth Wilson

Guide to Typefully Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationTypefully's API settings (where its docs say to re-enable a disabled webhook)
MethodPOST with a JSON body
EventsSeven draft events, in any combination
Signature headerX-Typefully-Signature, sha256=<hex>
Timestamp headerX-Typefully-Timestamp, a Unix timestamp of when the event was generated
Event headerX-Typefully-Event
SchemeHMAC-SHA256
Signed content<timestamp>.<raw body>
Payload{ event, data }, compact JSON with sorted keys
AcknowledgementAny 2xx
RetriesUp to 4 retries (5 attempts) with exponential backoff over 1 hour
Auto-disableAfter 100 consecutive failures, with an email
Response timeoutNot documented

Common events

EventDescription
draft.createdA new draft is created
draft.plannedA draft is planned: it has a date but won't publish until confirmed
draft.scheduledA draft is scheduled for publishing
draft.publishedA draft is published successfully
draft.status_changedAny status transition, including planned, scheduled and published
draft.tags_changedA draft's tags are modified
draft.deletedA 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

  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.

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-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 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_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 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.