Skip to main content

Webhooks & Notifications

Upload-Post allows you to receive real-time notifications about upload statuses and social account connection changes. This eliminates the need to poll endpoints for status updates.

Configuration

You can configure notifications in the Upload-Post Dashboard:

Configure Notifications

You can choose to receive notifications via:

  • Webhook: A POST request sent to your server with a JSON payload.
  • Telegram: A message sent to a configured Telegram chat.

Configuration via API

You can also configure your notification preferences programmatically using the API.

Endpoint: POST https://app.upload-post.com/api/uploadposts/users/notifications

Authentication: Requires a valid API Key.

Request Body:

{
"channels": {
"webhook": true,
"telegram": false
},
"webhook_url": "https://your-server.com/webhook-endpoint",
"telegram_chat_id": "123456789",
"webhook_events": {
"upload_completed": true,
"social_account_connected": true,
"social_account_disconnected": true,
"social_account_reauth_required": true
}
}

Response:

{
"success": true,
"notifications": {
"channels": { "webhook": true, ... },
"webhook_url": "...",
"webhook_secret": "whsec_3f9a…",
"webhook_events": { "upload_completed": true, "social_account_connected": true, ... },
...
}
}

The same endpoint supports GET (read the current settings, including webhook_secret) and DELETE (remove the account webhook: clears webhook_url, webhook_events and webhook_secret and turns the webhook channel off; Telegram/Slack/WhatsApp settings are untouched).

Per-profile webhooks

Agencies and white-label integrators can route each profile's (tenant's) events to a different URL. A profile webhook fires in addition to the account webhook, has its own optional webhook_events filter, and is signed with the same account webhook_secret. Leave the account-level webhook channel off if you only want per-profile delivery.

MethodEndpointBody / query
GET/api/uploadposts/users/profile-webhook?profile_username=acme
POST/api/uploadposts/users/profile-webhook{"profile_username": "acme", "webhook_url": "https://…", "webhook_events": {"upload_completed": true}}webhook_events is optional (omit = all events, null = reset)
DELETE/api/uploadposts/users/profile-webhook{"profile_username": "acme"}
curl -X POST https://api.upload-post.com/api/uploadposts/users/profile-webhook \
-H "Authorization: Apikey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"profile_username": "acme", "webhook_url": "https://acme.example.com/upload-post"}'

Response: {"success": true, "profile_username": "acme", "webhook_url": "https://acme.example.com/upload-post", "webhook_events": null}. Authentication: account API key or dashboard session (the profile-scoped connect JWT cannot change webhooks).

Verifying signatures

Every webhook request (account and per-profile) carries these headers:

HeaderValue
X-Upload-Post-Signaturesha256=<hex HMAC-SHA256> — present whenever the account has a webhook_secret
X-Upload-Post-TimestampUnix time (seconds) when the request was signed
X-Upload-Post-EventEvent name, e.g. upload_completed
X-Upload-Post-DeliveryUnique id of this delivery — use it to de-duplicate
User-AgentUpload-Post-Webhooks/1.0

The signature is HMAC_SHA256(webhook_secret, "<timestamp>.<raw body>"). Verify it against the raw request bytes — do not parse and re-serialise the JSON first — reject timestamps older than ~5 minutes to prevent replays, and compare with a constant-time function.

The secret is created automatically the first time you save a webhook_url (or a profile webhook). Read it with GET /api/uploadposts/users/notifications or in the dashboard under Notifications → Signing secret. Rotate it with:

curl -X POST https://api.upload-post.com/api/uploadposts/users/webhook-secret \
-H "Authorization: Apikey YOUR_API_KEY"
# → {"success": true, "webhook_secret": "whsec_…"}

Rotation takes effect immediately: deploy the new secret on your receiver first (accept both during the switch) to avoid rejected deliveries. Deliveries made before a secret existed carry no signature header.

Node.js (Express)

import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.UPLOAD_POST_WEBHOOK_SECRET;

app.post("/upload-post", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-Upload-Post-Timestamp");
const sig = (req.header("X-Upload-Post-Signature") || "").replace("sha256=", "");
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const expected = crypto.createHmac("sha256", SECRET).update(`${ts}.`).update(req.body).digest("hex");
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body);
// handle event…
res.sendStatus(200);
});

Python (Flask)

import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = "whsec_…"

@app.post("/upload-post")
def upload_post_webhook():
ts = request.headers.get("X-Upload-Post-Timestamp", "")
sig = request.headers.get("X-Upload-Post-Signature", "").removeprefix("sha256=")
if not ts or abs(time.time() - int(ts)) > 300:
abort(400)
expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json()
return "", 200

Webhook Events

You can subscribe to specific event types using the webhook_events object. Set each event key to true to receive it, or false to disable it. If omitted, all events are enabled by default.

EventDescription
upload_completedFired when an upload process completes (success or failure).
social_account_connectedFired when a social account is connected or reconnected.
social_account_disconnectedFired when a social account is disconnected (manually or automatically).
social_account_reauth_requiredFired when a social account requires re-authentication.

Webhook Payloads

upload_completed

Sent when an upload process completes (whether successfully or with a failure).

{
"event": "upload_completed",
"job_id": "a1b2c3d4e5f6...",
"user_email": "[email protected]",
"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"
}

Field Descriptions

FieldTypeDescription
eventstringThe type of event: upload_completed.
job_idstringThe persistent job identifier returned when the post was created or scheduled. Use this to correlate webhook events with your original API requests. Only present when the upload was triggered via the API with a job_id.
user_emailstringThe email address of the user who initiated the upload.
profile_usernamestringThe username of the profile associated with the upload.
platformstringThe social platform where the post was uploaded (e.g., instagram, youtube, tiktok).
media_typestringThe type of media uploaded (video, photo, or text).
titlestringThe title provided for the post.
captionstringThe caption or description of the post.
resultobjectAn object containing the outcome of the upload attempt.
result.successbooleantrue if the upload was successful, false otherwise.
result.urlstringThe direct URL to the published post (if available and successful).
result.publish_idstringThe ID assigned to the post by the platform.
result.errorstringA description of the error if the upload failed.
created_atstringThe timestamp of the event in ISO 8601 format.

social_account_connected

Sent when a social account is successfully connected or reconnected via OAuth.

{
"event": "social_account_connected",
"user_email": "[email protected]",
"platform": "instagram",
"account_name": "my_instagram_handle",
"status": "connected",
"profile_username": "your_profile_username",
"created_at": "2024-03-15T14:30:00.000000"
}

social_account_disconnected

Sent when a social account is disconnected. This can happen due to:

  • Manual disconnection by the user
  • Automatic disconnection due to persistent authentication failures (account blocked)
{
"event": "social_account_disconnected",
"user_email": "[email protected]",
"platform": "tiktok",
"account_name": "my_tiktok_handle",
"status": "disconnected",
"profile_username": "your_profile_username",
"reason": "manual_disconnect",
"created_at": "2024-03-15T14:30:00.000000"
}

social_account_reauth_required

Sent when a social account's access token can no longer be refreshed and the user must re-authenticate. This typically happens when:

  • The refresh token has expired
  • The user revoked access on the platform
  • Multiple consecutive token refresh attempts have failed
{
"event": "social_account_reauth_required",
"user_email": "[email protected]",
"platform": "youtube",
"account_name": "UCxxxxxxxxx",
"status": "reauth_required",
"reason": "token_refresh_threshold_exceeded",
"created_at": "2024-03-15T14:30:00.000000"
}

Connection Status Event Fields

FieldTypeDescription
eventstringThe event type: social_account_connected, social_account_disconnected, or social_account_reauth_required.
user_emailstringThe email address of the account owner.
platformstringThe social platform (e.g., instagram, youtube, tiktok, x, linkedin, facebook, threads, pinterest, reddit, bluesky, snapchat, google_business, tiktok_business, discord, telegram, slack, mastodon, nostr, lemmy, devto, hashnode, wordpress, whop, listmonk).
account_namestringThe account identifier on the platform (e.g., username, channel ID).
statusstringThe new connection status: connected, disconnected, or reauth_required.
profile_usernamestringThe Upload-Post profile associated with this account (if applicable).
reasonstringAdditional context for the status change (e.g., manual_disconnect, account_blocked, token_refresh_threshold_exceeded, max_auth_strikes). Only present for disconnected and reauth_required events.
created_atstringThe timestamp of the event in ISO 8601 format.

Usage Notes

  • Idempotency: While we strive to deliver each notification exactly once, you should handle potential duplicate events based on your own unique identifiers if necessary (though post_id or publish_id can serve this purpose for successful posts).
  • Security: Use HTTPS and verify the signature of every delivery. Use X-Upload-Post-Delivery to de-duplicate retries.
  • Event Filtering: Use the webhook_events field in your notification settings to subscribe only to the events you need. If not specified, all events are enabled by default.
  • Replacing Polling: If you were previously polling the profile endpoint to check connection status, subscribe to social_account_connected, social_account_disconnected, and social_account_reauth_required events instead.