Register a webhook URL
For now, attach the callback URL to each async request body:
POST /api/v1/generate-and-post
{
"topic": "...",
"platforms": ["tiktok"],
"webhook_url": "https://your-app.example.com/webhooks/reelsbuilder"
}Account-level webhook subscriptions (one URL for all your jobs) are coming in Phase 2.
Payload shape
POST https://your-app.example.com/webhooks/reelsbuilder
Content-Type: application/json
X-RB-Signature: t=1715797200,v1=8a9b3c5d2e1f...
X-RB-Event-Id: evt_01HKZ2Q9YX...
X-RB-Event-Type: video.completed
{
"event_id": "evt_01HKZ2Q9YX...",
"event_type": "video.completed",
"created_at": "2026-05-15T20:00:00.000Z",
"data": {
"job_id": "job_01HKZ2P0...",
"status": "completed",
"video_url": "https://cdn.reelsbuilder.ai/v/...",
"duration_sec": 28.4,
"platforms": [
{ "platform": "tiktok", "status": "posted", "external_id": "7..." },
{ "platform": "instagram", "status": "posted", "external_id": "..." }
],
"credits_used": 12,
"credits_remaining": 988
}
}Signature verification
Every webhook is signed with HMAC-SHA256 using a secret derived from your API key (the first 32 hex characters of the key's SHA-256 hash — stable per key, rotates when you rotate the key). The signature is sent in the X-RB-Signature header.
Format: t=<unix-seconds>,v1=<hex-hmac>
The HMAC payload is <timestamp>.<raw-request-body> — concatenate the timestamp, a literal period, and the unparsed JSON body.
Verify (TypeScript / Node)
import crypto from "node:crypto";
function deriveWebhookSecret(apiKey: string): string {
return crypto.createHash("sha256").update(apiKey).digest("hex").slice(0, 32);
}
export function verifyWebhook(req: {
rawBody: string;
headers: Record<string, string>;
}, apiKey: string, toleranceSec = 300): boolean {
const sigHeader = req.headers["x-rb-signature"];
if (!sigHeader) return false;
const parts = Object.fromEntries(
sigHeader.split(",").map((kv) => kv.trim().split("=")),
);
const t = Number.parseInt(parts.t ?? "0", 10);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const secret = deriveWebhookSecret(apiKey);
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${req.rawBody}`)
.digest("hex");
const provided = parts.v1 ?? "";
if (expected.length !== provided.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(provided, "hex"),
);
}Verify (Python)
import hashlib, hmac, time
def derive_webhook_secret(api_key: str) -> str:
return hashlib.sha256(api_key.encode()).hexdigest()[:32]
def verify_webhook(raw_body: bytes, signature_header: str, api_key: str, tolerance_sec: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in signature_header.split(","))
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_sec:
return False
secret = derive_webhook_secret(api_key)
payload = f"{t}.{raw_body.decode()}".encode()
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Retry policy
If your endpoint returns a non-2xx status or doesn't respond within 10 seconds, ReelsBuilder retries with exponential backoff:
- Attempt 1: immediate
- Attempt 2: after 30s
- Attempt 3: after 2 min
- Attempt 4: after 10 min
- Attempt 5: after 1 hour
- Attempt 6: after 6 hours
After 6 attempts (~7 hours total) the event is marked permanently failed and surfaced in the developer dashboard for manual replay.
Replay safety
Every event carries a stable event_id in both the body and the X-RB-Event-Id header. Persist this ID and drop duplicates — retries deliver the same event ID, so a simple unique-index check is sufficient.
Event types
video.completed— Generation finished, file is on CDN.video.failed— Generation failed; credits refunded.post.completed— Single-platform post succeeded; includes external ID and platform URL.post.failed— Single-platform post failed; other platforms in the same job may still succeed.job.completed— Entire fan-out finished (all platforms attempted, success or failure recorded for each).
Testing locally
Use ngrok or cloudflared to expose your local listener. Use a sandbox key (rb_test_*) to avoid charging real credits during integration.
Best practices
- Verify the signature on every request. Unsigned and invalid-signature requests should return 401 — never trust the payload before verification.
- Respond quickly (under 3s). Queue the work in a job system; return 200 immediately. Long handlers cause retries.
- Idempotent handlers. Use the
event_idas a deduplication key. - Verify timestamp freshness. Reject events with
tmore than 5 minutes off — this defeats replay attacks. - Log raw bodies + signatures for at least 30 days. You need them to debug verification failures.
See also
- Idempotency — for outbound POST safety
- Errors — error codes you might receive when registering webhook URLs