Guide to Upload-Post Webhooks: Features and Best Practices
Upload-Post is an API for publishing videos, photos and text to many social networks with one request. Its webhooks tell your application when an upload has finished on each platform, when a connected social account is disconnected or needs re-authorising, and, if you opt in, when someone sends a DM or leaves a comment on a connected account. They replace polling the upload status endpoint.
This guide covers how Upload-Post webhooks are configured and signed, the events you will receive, the single-attempt delivery model, and the practices that keep an Upload-Post receiver from missing results.
What are Upload-Post webhooks?
Upload-Post sends notifications through several channels, including Telegram, and a webhook is the one that reaches your own server. You set one webhook URL for the account and choose which events it receives. Agencies and white-label integrators can also set a separate URL for each profile, which fires in addition to the account webhook.
Each event is a JSON POST with these headers:
| Header | Value |
|---|---|
X-Upload-Post-Signature | sha256=<hex HMAC-SHA256>, present whenever the account has a webhook secret |
X-Upload-Post-Timestamp | Unix time in seconds when the request was signed |
X-Upload-Post-Event | The event name, such as upload_completed |
X-Upload-Post-Delivery | A unique ID for this delivery |
User-Agent | Upload-Post-Webhooks/1.0 |
The signature covers the timestamp, a full stop and the raw body. The secret starts with whsec_, which looks like a Standard Webhooks secret, but Upload-Post uses the whole string, prefix included, as a plain HMAC key. Standard Webhooks and Svix libraries won't verify it.
Upload-Post webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard Notifications, or POST /api/uploadposts/users/notifications |
| Per-profile webhooks | /api/uploadposts/users/profile-webhook, signed with the account secret |
| Subscription | webhook_events object; all events by default except DMs and comments |
| Method | POST, JSON body |
| Signature header | X-Upload-Post-Signature, sha256= followed by lowercase hex |
| Timestamp header | X-Upload-Post-Timestamp, Unix seconds |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | {X-Upload-Post-Timestamp}.{raw body} |
| Secret | whsec_..., created when a webhook URL is first saved, used whole as UTF-8 |
| Rotation | POST /api/uploadposts/users/webhook-secret, takes effect immediately |
| Timeout | 10 seconds |
| Retries | None. Each event is sent once |
| Circuit breaker | After 5 consecutive failures the channel pauses for 30 minutes |
Common events
| Event | Fires when |
|---|---|
upload_completed | An upload finishes on a platform, whether it succeeded or failed |
social_account_connected | A social account is connected or reconnected |
social_ | A social account is disconnected, by the user or automatically |
social_ | A social account's token can no longer be refreshed |
dm_received | Someone sends a DM to a connected Instagram account. Opt-in |
comment_received | Someone comments on a post from a connected Instagram, TikTok or LinkedIn Company Page account, with Threads rolling out. Opt-in |
dm_received and comment_received arrive only if you list them explicitly in webhook_events. Leaving webhook_events empty turns on the other four, not these. They go only to webhooks, never to Telegram, Slack or WhatsApp.
Here is Upload-Post's upload_completed example:
{
"event": "upload_completed",
"request_id": "f7e8d9c0b1a2...",
"job_id": "a1b2c3d4e5f6...",
"external_id": "cms-post-8841",
"user_email": "user@example.com",
"profile_username": "your_profile_username",
"platform": "instagram",
"media_type": "video",
"title": "My Awesome Video",
"caption": "Check this out! #cool",
"result": {
"success": true,
"url": "https://www.instagram.com/p/C1234567890/",
"publish_id": "17987654321098765",
"post_id": "17987654321098765",
"error": null
},
"created_at": "2024-03-15T14:30:00.000000"
}
eventnames the event. Route on it, or on theX-Upload-Post-Eventheader.request_idmatches the upload request, andjob_idandexternal_idcarry your own identifiers when you supplied them.platformsays which network the result is for. Each event carries a single platform and a singleresult.result.successisfalsefor a failed upload, with the reason inresult.error.
Account events carry platform, account_name, status and, for disconnections and re-authorisation, a reason such as manual_disconnect, account_blocked or token_. Comment events share one shape across networks, but some fields differ: TikTok and LinkedIn give timestamp in milliseconds, while Threads gives ISO 8601.
Setting up Upload-Post webhooks
- In the Upload-Post dashboard, open Notifications, turn on the Webhook channel and enter your URL, for example
https://example.com/webhooks/upload-post. - Choose the events. Tick
dm_receivedandcomment_receivedonly if you want them. - Save. Upload-Post creates the signing secret the first time a webhook URL is saved.
- Copy the secret from Notifications > Signing secret, including the
whsec_prefix, and store it server-side, for example asUPLOAD_.POST_ WEBHOOK_ SECRET
To do the same with the API:
curl -X POST https://app.upload-post.com/api/uploadposts/users/notifications \
-H "Authorization: Apikey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channels": { "webhook": true },
"webhook_url": "https://example.com/webhooks/upload-post",
"webhook_events": {
"upload_completed": true,
"social_account_disconnected": true,
"social_account_reauth_required": true
}
}'
The response includes webhook_secret, and GET on the same endpoint returns it later.
If you use Hookdeck, create a source with the Upload-Post source type and use the Hookdeck source URL as the webhook URL. Hookdeck's setup notes for this source say: "Paste the whole secret, including the whsec_ prefix. Upload-Post uses it as a plain HMAC key." The source accepts POST only, which is the method Upload-Post uses.
Securing Upload-Post webhooks
To verify a delivery, read X-Upload-Post-Timestamp and X-Upload-Post-Signature, and strip sha256= from the signature. Build the signed content from the timestamp header exactly as received, a . and the raw request body. Compute an HMAC-SHA256 with the whole secret as the key, encode it as hex and compare it with the header in constant time.
Upload-Post's own samples reject timestamps older than 300 seconds with a 400. In the code below that check is optional and off by default, for a reason covered in the next section.
const crypto = require('crypto');
const express = require('express');
function verifyUploadPostSignature(rawBody, timestamp, signatureHeader, secret, toleranceSeconds = 0) {
if (!timestamp || !signatureHeader || !secret) return false;
const signature = signatureHeader.replace(/^sha256=/i, '');
if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false;
// Optional replay check, off by default.
if (toleranceSeconds > 0) {
if (!/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
}
// The whole secret, whsec_ prefix included, is the key.
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp, 'latin1')
.update('.')
.update(rawBody)
.digest('hex');
const received = Buffer.from(signature.toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/upload-post', express.raw({ type: '*/*' }), (req, res) => {
const ok = verifyUploadPostSignature(
req.body,
req.get('x-upload-post-timestamp'),
req.get('x-upload-post-signature'),
process.env.UPLOAD_POST_WEBHOOK_SECRET
);
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// Queue the event, then answer well inside 10 seconds.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import re
import time
HEX_64 = re.compile(r"[0-9a-fA-F]{64}")
def verify_upload_post_signature(raw_body: bytes, timestamp, signature_header, secret: str,
tolerance_seconds: int = 0) -> bool:
if not timestamp or not signature_header or not secret:
return False
signature = re.sub(r"^sha256=", "", signature_header, count=1, flags=re.IGNORECASE)
if not HEX_64.fullmatch(signature):
return False
# Optional replay check, off by default.
if tolerance_seconds > 0:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance_seconds:
return False
signed = timestamp.encode("latin-1") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_upload_post_signature(raw_body, request.headers.get("x-upload-post-timestamp"),
# request.headers.get("x-upload-post-signature"),
# os.environ["UPLOAD_POST_WEBHOOK_SECRET"])
Hookdeck's Upload-Post source type checks the same signature: HMAC-SHA256 over {X-Upload-Post-Timestamp}.{body}, hex, keyed with the whole secret. It applies no timestamp tolerance. A request that fails the check is marked as failed verification and is not delivered to your destinations.
Make Upload-Post webhooks production-ready. Hookdeck Event Gateway verifies the
X-Upload-Post-Signatureheader on every delivery, stores each upload result and account event, and delivers it to your service with retries you control.
Upload-Post webhook limitations and pain points
One attempt, no retries
The Problem: An upload result that arrives during a deploy is gone for good, and your system never learns that the post went live, or that it failed.
Why It Happens: Upload-Post sends each event once. If your endpoint takes longer than 10 seconds, refuses the connection or answers with a non-2xx, "that event is not retried or queued for later". Upload-Post describes webhooks as a fast path and tells you to keep a fallback.
Workarounds:
- Answer with a
2xxas soon as the signature checks out, and do the work afterwards. - Reconcile on a schedule with the Upload Status or Upload History endpoints, matching on
request_id.
How Hookdeck Can Help: Hookdeck accepts each delivery as it arrives and stores it, so the one attempt Upload-Post makes is the one Hookdeck receives. Hookdeck then delivers to your service with retries on your schedule, and you can replay anything that failed.
The circuit breaker drops events
The Problem: A run of errors is followed by half an hour of silence, even after your endpoint recovers.
Why It Happens: After 5 consecutive failed deliveries, Upload-Post pauses the webhook channel for 30 minutes. "Deliveries skipped while paused are not retried later." A short outage becomes a 30-minute gap.
Workarounds:
- Keep the endpoint fast and always available, and process asynchronously.
- After any pause, reconcile the missing window through Upload History.
How Hookdeck Can Help: Because Hookdeck answers each delivery when it arrives, failures in your own service don't count towards Upload-Post's five.
The five-minute window and replays
The Problem: A delivery that you replay from Hookdeck is rejected by your own code, even though its signature is valid.
Why It Happens: Upload-Post's samples reject timestamps older than 300 seconds. Hookdeck retries and replays resend the original headers, so they carry the timestamp of the first send.
Workarounds:
- Leave the timestamp check off and rely on idempotency instead.
- If you turn it on, choose a window longer than the longest delay you allow between the original send and a retry or replay.
How Hookdeck Can Help: Hookdeck applies no timestamp tolerance, so retried and replayed deliveries still pass verification at the source.
Rotation takes effect immediately
The Problem: Deliveries fail verification right after you rotate the secret.
Why It Happens: Rotating with POST /api/uploadposts/users/webhook-secret switches the secret at once, and Upload-Post has one secret for the account and all its profile webhooks. Any receiver still on the old secret fails until it is updated, and with no retries those events are not resent.
Workarounds:
- Rotate at a quiet time, and update every receiver straight away.
- Accept both the old and new secret for a short period during the switch, as Upload-Post suggests.
- Reconcile the rotation window through Upload History.
How Hookdeck Can Help: With Hookdeck in front, the secret lives in one Hookdeck source rather than in each receiving service, so there is only one place to update.
Best practices
Verify against the raw body
The signature covers the exact bytes Upload-Post sent. In Express, use express.raw({ type: '*/*' }) on the webhook route. In a Next.js route handler, use Buffer.from(await request.arrayBuffer()). In FastAPI, read await request.body(). Parse JSON only after verification passes.
Use the whole secret
Keep the whsec_ prefix and don't base64-decode anything. If verification fails on every delivery, check this first.
Deduplicate on your own identifiers
X-Upload-Post-Delivery is unique per delivery and is for logging. For idempotency, Upload-Post points to your own identifiers, such as result.post_id or result.publish_id for successful posts. For failed uploads, a key built from request_id and platform does the same job. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Upload-Post waits 10 seconds and never tries again. Verify, persist the event, return 200, and update your records afterwards. See why to process webhooks asynchronously.
Act on account events
social_ and social_ mean future uploads to that account will fail. Prompt the user to reconnect when they arrive, rather than waiting for the next failed upload.
Conclusion
Upload-Post signs each delivery with a sha256= hex HMAC of the timestamp and raw body, keyed with the whole whsec_ secret. It sends each event once, with a 10-second timeout and a circuit breaker that drops events while paused, so a fast, always-available endpoint and a reconciliation job matter more here than with most providers.
Hookdeck Event Gateway verifies every Upload-Post signature and holds each event until your service has processed it, so an upload result isn't lost to a deploy or a slow handler.
Get started with Hookdeck for free and handle Upload-Post webhooks reliably in minutes.