Webhooks
Receive real-time notifications when generations complete.
Get notified instantly when generations complete instead of polling.
Webhook configuration is per-workspace — each workspace can have its own webhook URL, secret, and subscribed events. See Workspaces for more on workspace scoping.
Setup
- Go to Settings → API in your dashboard
- Make sure you've selected the correct workspace
- Enter your webhook URL
- Select events to receive
- Save your configuration
Your webhook URL must:
- Use HTTPS
- Respond within 10 seconds
- Return a 2xx status code
Events
| Event | Description |
|---|---|
generation.completed | Caption generation completed |
generation.failed | Caption generation failed |
image_generation.completed | Image generation completed |
image_generation.failed | Image generation failed |
post.published | Post published successfully |
post.failed | Post publishing failed |
Payload Examples
generation.completed
{
"event": "generation.completed",
"timestamp": "2026-01-10T18:35:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"generation_id": "660f9500-f30c-52e5-b827-557766551111",
"status": "completed",
"outputs": {
"tiktok": "🌙 Dark mode activated! POV: your eyes at 2am... #darkmode #tech",
"instagram": "✨ Dark mode is here!\n\nYour late-night scrolling just got easier... #DarkMode #AppUpdate"
}
}
}YouTube outputs have a title and description object. All other platforms are plain strings
with hashtags included.
generation.failed
{
"event": "generation.failed",
"timestamp": "2026-01-10T18:35:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"generation_id": "660f9500-f30c-52e5-b827-557766551111",
"status": "failed",
"error_code": "GENERATION_FAILED",
"error_message": "Failed to generate content"
}
}image_generation.completed
{
"event": "image_generation.completed",
"timestamp": "2026-01-10T18:36:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"image_generation_id": "7a8b9c0d-e1f2-3456-abcd-ef7890123456",
"status": "completed",
"images": [
{
"media_id": "img-001-abcd-efgh"
}
]
}
}image_generation.failed
{
"event": "image_generation.failed",
"timestamp": "2026-01-10T18:36:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"image_generation_id": "7a8b9c0d-e1f2-3456-abcd-ef7890123456",
"status": "failed",
"error_code": "IMAGE_GENERATION_FAILED",
"error_message": "Failed to generate images"
}
}post.published
Sent when at least one item is published successfully. Individual items may have different statuses — check each item's status field.
{
"event": "post.published",
"timestamp": "2026-01-10T18:40:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"post_id": "post-660f9500-f30c-52e5-b827-557766551111",
"status": "published",
"items": [
{
"item_id": "item-001",
"post_type": "instagram-reel",
"platform": "instagram",
"status": "posted",
"platform_post_id": "17898455678012345"
},
{
"item_id": "item-002",
"post_type": "tiktok-video",
"platform": "tiktok",
"status": "failed",
"error": "Access token expired. Please reconnect your TikTok account."
}
]
}
}A post.published event may contain a mix of successful and failed items (partial success).
Always check each item's status field individually.
post.failed
{
"event": "post.failed",
"timestamp": "2026-01-10T18:40:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"post_id": "post-660f9500-f30c-52e5-b827-557766551111",
"status": "failed",
"items": [
{
"item_id": "item-001",
"post_type": "instagram-reel",
"platform": "instagram",
"status": "failed",
"error": "Access token expired. Please reconnect your Instagram account."
}
]
}
}Signature Verification
All webhooks are signed with HMAC-SHA256 for security.
Signature Header
X-PowerPost-Signature: t=1736534100,v1=abc123def456...t— Unix timestamp when the webhook was sentv1— HMAC-SHA256 signature
Verification (Node.js)
import crypto from 'crypto'
function verifyWebhook(rawBody, signature, secret) {
const [tPart, vPart] = signature.split(',')
const timestamp = tPart.split('=')[1]
const providedSig = vPart.split('=')[1]
// Verify timestamp is recent (within 5 min)
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false // Replay attack
}
// Compute expected signature
const signedPayload = `${timestamp}.${rawBody}`
const expectedSig = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex')
// Timing-safe comparison
return crypto.timingSafeEqual(Buffer.from(providedSig), Buffer.from(expectedSig))
}Verification (Python)
import hmac
import hashlib
import time
def verify_webhook(raw_body: str, signature: str, secret: str) -> bool:
parts = dict(p.split('=') for p in signature.split(','))
timestamp = parts['t']
provided_sig = parts['v1']
# Check timestamp
if abs(time.time() - int(timestamp)) > 300:
return False
# Compute signature
signed_payload = f"{timestamp}.{raw_body}"
expected_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(provided_sig, expected_sig)Webhook Secret
Your webhook secret is shown when you configure webhooks. Store it securely — it's used to verify that webhooks are genuinely from PowerPost.
Delivery Policy
Webhook delivery is best-effort with a single attempt. If your endpoint returns a non-2xx status code, times out (10 seconds), or is unreachable, the delivery is marked as failed.
You can view failed deliveries in your dashboard. Use the relevant GET endpoints to poll for status as a fallback if your webhook endpoint experiences downtime.
Debugging
Check the Webhook Deliveries section in your dashboard to see recent delivery attempts, status codes, and response bodies.