01
Events
- upload_completed
- One per platform, when its post went live or failed.
- social_account_connected
- An account was connected to a profile.
- social_account_disconnected
- An account was disconnected.
- social_account_reauth_required
- An account needs reconnecting before it can post again.
02
Add an endpoint
In the dashboard under Settings → Webhooks, or with the API (the key needs the webhooks scope and no profile limit):
curl -X POST https://app.reelspan.com/api/uploadposts/users/notifications \
-H "Authorization: Apikey $REELSPAN_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://example.com/reelspan",
"webhook_events": ["upload_completed", "social_account_reauth_required"]
}'The endpoint gets a signing secret that starts with whsec_. A profile can have its own endpoint too (/api/uploadposts/users/profile-webhook). Send a test with POST /api/uploadposts/users/notifications/test.
03
Payload
Every delivery is a JSON POST with the event name, its data and a timestamp:
{
"event": "upload_completed",
"request_id": "post_8fK2…",
"job_id": null,
"external_id": "your-id-42",
"profile_username": "my-brand",
"platform": "tiktok",
"media_type": "video",
"title": "Golden hour, no filter",
"result": { "success": true, "url": "https://www.tiktok.com/@…", "post_id": "73…" },
"created_at": "2026-11-07T12:00:03.000Z"
}Account events carry platform, account_name, status and profile_username; a disconnect adds a reason: manual_disconnect, token_refresh_failed or permission_revoked.
04
Verify the signature
Each request has these headers:
- x-reelspan-event
- The event name.
- x-reelspan-delivery
- A unique id; use it to ignore duplicates.
- x-reelspan-timestamp
- Unix seconds when it was sent.
- x-reelspan-signature
- sha256= followed by the hex HMAC-SHA256 of "{timestamp}.{raw body}" with your secret.
Check it against the raw body, before parsing, and reject old timestamps:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, headers, secret) {
const ts = headers['x-reelspan-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes
const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const got = headers['x-reelspan-signature'] ?? '';
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}import hashlib, hmac, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers["x-reelspan-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
signed = f"{ts}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(headers.get("x-reelspan-signature", ""), expected)05
Retries and pausing
Answer with any 2xx within 10 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours: 8 attempts in all. Do slow work after you answer.
After 20 deliveries in a row fail every retry, the endpoint is paused and the workspace gets a notification and an email. Fix it, then turn it back on in Settings. The delivery log there shows each attempt and lets you redeliver one (POST /api/uploadposts/webhooks/deliveries/{id}/redeliver).
06