Agent skill
Upload-Post Webhooks Skill
Receive and verify Upload-Post webhooks (upload-post.com, the API for publishing to many social networks). Use when setting up an Upload-Post webhook handler, debugging X-Upload-Post-Signature verification, or handling upload_completed, social_account_connected, social_account_disconnected, social_account_reauth_required, dm_received and comment_received events. The signature is a hex HMAC-SHA256 over the X-Upload-Post-Timestamp header, a dot, and the raw body. It is not Standard Webhooks.
Install this skill
npx skills add hookdeck/webhook-skills --skill upload-post-webhooks
Upload-Post (upload-post.com) is an API for publishing content to many social networks. Its webhooks report upload results and social-account connection changes, plus opt-in Instagram DMs and comments (Instagram, TikTok, LinkedIn Company Pages).
When to Use This Skill
- How do I receive Upload-Post webhooks?
- How do I verify the
X-Upload-Post-Signatureheader? - How do I handle
upload_completed,social_oraccount_ reauth_ required comment_receivedevents? - Why does my Upload-Post webhook signature verification fail?
- Does Upload-Post use Standard Webhooks? (No.)
Verification (core)
Upload-Post sends two headers: X-Upload-Post-Signature: sha256=<hex> (present whenever the account has a webhook_secret) and X-Upload-Post-Timestamp (Unix seconds). The signature is the lowercase hex HMAC-SHA256 of <timestamp>.<raw body>.
Paste the whole secret, including the whsec_ prefix. Upload-Post uses it as a plain HMAC key.
The key is the secret's UTF-8 bytes, used whole: nothing is base64-decoded, so do not use the standardwebhooks or svix libraries. Build the signed message as bytes (the timestamp header value as latin1, then ., then the raw body) and never decode and re-encode the body.
Node:
const crypto = require('crypto');
function verify(rawBody, signature, timestamp, secret) {
if (!signature || !timestamp || !secret) return false;
const hex = signature.startsWith('sha256=') ? signature.slice(7) : signature;
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return false;
const signed = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(signed).digest();
const received = Buffer.from(hex.toLowerCase(), 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Python:
import hashlib, hmac, re
def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if not signature or not timestamp or not secret:
return False
digest = signature[7:] if signature.startswith("sha256=") else signature
if not re.fullmatch(r"[0-9a-fA-F]{64}", digest):
return False
signed = b"".join([timestamp.encode("latin-1"), b".", raw_body])
expected = hmac.new(secret.encode(), signed, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(digest.lower()), expected)
A missing or empty header, a malformed value or a mismatch is a 401. In Express use express.raw({ type: '*/*' }), in Next.js await request.arrayBuffer(), in FastAPI await request.body(), and parse the JSON only after the check passes.
Timestamp window (opt-in): the examples do not reject old timestamps by default. Hookdeck retries and replays resend the original headers, and Hookdeck itself applies no tolerance, so a fixed window would reject legitimate retries. Set UPLOAD_ (unset or 0 = off) to reject deliveries whose X-Upload-Post-Timestamp is further than that many seconds from now.
Deliveries made before a secret existed carry no signature header and fail verification.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Dispatch on the body's event field. The examples handle these and log the rest.
| Event | Triggered When |
|---|---|
upload_completed | An upload finished (result per platform) |
social_account_connected | A social account was connected to a profile |
social_ | A social account was disconnected (reason included) |
social_ | A connected account needs to be re-authorized |
dm_received | An Instagram DM arrived (opt-in) |
comment_received | A comment arrived on a connected Instagram, TikTok or LinkedIn Company Page account (opt-in; Threads rolling out) |
For payload fields, see Upload-Post webhooks
Important Headers
| Header | Description |
|---|---|
X-Upload-Post-Signature | sha256=<hex> HMAC-SHA256, sent when a webhook_secret is set |
X-Upload-Post-Timestamp | Unix time (seconds) when the request was signed; part of the signed content |
X-Upload-Post-Event | Event name (same as the body's event) |
Delivery
One attempt per event with a 10 s timeout. A refused connection or non-2xx response loses the event, and after 5 consecutive failures the channel pauses for 30 minutes. Respond quickly and process asynchronously.
Environment Variables
UPLOAD_POST_WEBHOOK_SECRET=whsec_your_webhook_secret # whole secret, whsec_ prefix included
UPLOAD_POST_WEBHOOK_TOLERANCE_SECONDS=0 # optional; 0 or unset = no timestamp window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 upload-post --path /webhooks/upload-post
Reference Materials
- references/overview.md - Upload-Post webhook concepts, events, delivery behaviour
- references/setup.md - Configure the webhook URL and secret
- references/verification.md - Signature verification details and gotchas