ReelspanDocsWebhooks

Webhooks

Instead of polling, give Reelspan a URL. It sends a signed POST for every platform’s result and every account change.
HMAC-SHA2568 attempts6 sections

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):

Register
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:

upload_completed
{
  "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:

verify.mjs
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));
}
verify.py
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

Next steps

Questions?Talk to a person.Contact us