All docs
Webhooks & integrations

Webhooks overview

Outgoing webhooks for slideshow events, plus the Stripe billing events we consume.

2 min readLast updated
Outgoing webhooks are live

Register HTTPS endpoints from /integrations/webhooks (UI) or POST /v0/api/webhooks (API). Each delivery is HMAC-SHA256 signed with your per-endpoint secret and carries the X-ViralSlides-Signature, X-ViralSlides-Event, and X-ViralSlides-Delivery-Id headers. Failed deliveries retry on a 1m / 5m / 30m / 2h / 12h backoff for up to six attempts.

Available events

  • slideshow.completed — generation finished; the payload carries the rendered slide image URLs, a thumbnail, and the share link.
  • slideshow.failed — generation failed (payload includes the error message; URL fields are empty).
  • quota.exceeded — the org hit its monthly slideshow / hook-refresh / apps quota.
Example payload (slideshow.completed)json
{
  "slideshow_id": "ss_01HQ...",
  "organization_id": "org_01HQ...",
  "app_id": "app_01HQ...",
  "app_name": "Tidy",
  "status": "COMPLETED",
  "hook_text": "POV: your app feed but tidier",
  "language": "en",
  "slide_count": 6,
  "template_set": "default",
  "thumbnail_url": "https://cdn.viralslides.app/orgs/org_01HQ.../slideshows/ss_01HQ.../slide-0.png",
  "image_urls": [
    "https://cdn.viralslides.app/orgs/org_01HQ.../slideshows/ss_01HQ.../slide-0.png",
    "https://cdn.viralslides.app/orgs/org_01HQ.../slideshows/ss_01HQ.../slide-1.png"
  ],
  "slides": [
    {
      "order": 0,
      "template": "HOOK",
      "layout_type": "hook-title",
      "title": "POV: your app feed but tidier",
      "subtitle": "",
      "label": "POV",
      "image_url": "https://cdn.viralslides.app/orgs/org_01HQ.../slideshows/ss_01HQ.../slide-0.png",
      "duration": 3
    }
  ],
  "share_url": "https://viralslides.app/share/shr_01HQ..."
}

URLs you can act on are included so you don't need a follow-up API call: `image_urls` is the ordered list of rendered slide PNGs, `slides` carries the same images plus per-slide metadata (order, template, layout, copy, duration), `thumbnail_url` is the first slide, and `share_url` is the public share page (null when sharing is disabled for the slideshow). To download everything as a ZIP, POST to the share endpoint: `POST /v0/public/share/{token}/zip`. On `slideshow.failed`, the URL fields are present but empty (no slides were rendered) and an `error` field describes the failure.

Verifying signatures

Compute HMAC-SHA256 of the raw request body using your endpoint secret and compare against the hex digest in X-ViralSlides-Signature (formatted as `sha256=<hex>`). Reject the request if the signatures don't match in constant time.

Stripe events we consume

Our Stripe webhook endpoint (https://api.viralslides.app/v0/public/billing/stripe-webhook) handles the events below. Each is signed and verified via STRIPE_WEBHOOK_SECRET; duplicates are deduped by event id.

EventEffect
customer.subscription.createdCreates a local subscription and mirrors plan + status onto the organization.
customer.subscription.updatedUpserts the subscription. Status maps to ACTIVE / TRIALING / PAST_DUE / CANCELED.
customer.subscription.deletedMarks the subscription CANCELED and suspends the organization. Triggers a notification email.
invoice.payment_failedSets the org status to PAST_DUE and emails the owner the amount due.
invoice.payment_succeededReactivates the org (ACTIVE) and rolls over usage_counters for the new billing period.
checkout.session.completedCaptures stripe_customer_id on the organization for fast linkage of the first checkout.

Idempotency

Every Stripe event id is recorded in processed_webhook_events on first delivery and short-circuited on subsequent retries. This means it's safe for Stripe to redeliver events — we'll just acknowledge with received: true, duplicate: true.

Share & oEmbed

Public share links are issued per slideshow from the Slideshows page or POST /v0/api/slideshows/:id/share. Tokens are 32-char base64url, unguessable, and can be rotated or password-protected. Share URLs follow the pattern https://viralslides.app/share/<token>; the same token is valid in the iframe-safe embed at /embed/<token>. oEmbed discovery is exposed at GET /v0/public/oembed?url=<share-url>&format=json.

Slack & Discord notifications

Add incoming webhook URLs from Slack or Discord at /integrations/notify. ViralSlides will format slideshow.completed / slideshow.failed / quota.exceeded events into native Slack attachments or Discord embeds and POST them to your channel. Test-send from the UI before relying on the channel in production.

MCP server

MCP server is live

The Model Context Protocol (MCP) server lets AI clients (Claude Code, Claude Desktop, Cursor, and anything that speaks MCP) list apps, browse hooks, and generate or publish slideshows through tool calls. It reuses the same vs_live_ API key as the REST API. See the dedicated MCP server guide in Webhooks & integrations for setup (use --scope user so it persists) and the full tool list.