Webhooks

Listen to template events and integrate with your application workflow


Get real-time notifications when users create, update, or delete templates in your embed. Perfect for syncing data, sending notifications, or triggering workflows.

Overview#

Webhooks send HTTP POST requests to your specified endpoint whenever:

  • A user creates a new template (template.create)
  • A user updates an existing template (template.update)
  • A user deletes a template (template.delete)

Setup#

1. Configure Webhook URL#

In your embed settings, add your webhook endpoint:

https://yourapp.com/api/studio-webhook

2. Handle Webhook Events#

Create an endpoint that accepts POST requests:

// Next.js API Route
export async function POST(request) {
  try {
    const body = await request.json();

    // Process the webhook
    console.log("Webhook received:", body);

    return Response.json({ success: true });
  } catch (error) {
    console.error("Webhook error:", error);
    return Response.json({ error: error.message }, { status: 500 });
  }
}

Event Types#

template.create#

Fired when a user creates a new template:

{
  "event_type": "template.create",
  "template": {
    "id": "abc123",
    "name": "My New Design",
    "workspace_id": "workspace_789",
    "user_id": "user_456",
    "canvas_data": {...},
    "canvas_width": 1080,
    "canvas_height": 1080,
    "current_version": "1.0",
    "thumbnail_url": "https://...",
    "description": "Template description",
    "pages_data": [
      {
        "id": "page_1",
        "name": "Page 1",
        "thumbnail_url": "https://...",
        "canvas_data": {...}
      }
    ],
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  },
  "workspace_id": "workspace_789",
  "user_id": "user_456",
  "timestamp": "2024-01-15T10:30:00Z"
}

template.update#

Fired when a user saves changes to a template:

{
  "event_type": "template.update",
  "template": {
    "id": "abc123",
    "name": "My Updated Design",
    "workspace_id": "workspace_789",
    "user_id": "user_456",
    "canvas_data": {...},
    "canvas_width": 1080,
    "canvas_height": 1350,
    "current_version": "1.1",
    "thumbnail_url": "https://...",
    "description": "Updated template description",
    "pages_data": [
      {
        "id": "page_1",
        "name": "Page 1",
        "thumbnail_url": "https://...",
        "canvas_data": {...}
      },
      {
        "id": "page_2",
        "name": "Page 2",
        "thumbnail_url": "https://...",
        "canvas_data": {...}
      }
    ],
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T11:45:00Z"
  },
  "workspace_id": "workspace_789",
  "user_id": "user_456",
  "timestamp": "2024-01-15T11:45:00Z"
}

template.delete#

Fired when a user deletes a template. The payload is lighter since the template data is no longer available:

{
  "event_type": "template.delete",
  "template": {
    "id": "abc123",
    "name": "My Template",
    "workspace_id": "789"
  },
  "workspace_id": "789",
  "user_id": "user_456",
  "timestamp": "2024-01-15T12:00:00Z"
}

Implementation Examples#

Express.js#

const express = require("express");
const app = express();

app.use(express.json());

app.post("/api/studio-webhook", (req, res) => {
  const { event_type, template, workspace_id, user_id } = req.body;

  console.log(`Event: ${event_type} for template ${template.id}`);

  // Process the event
  switch (event_type) {
    case "template.create":
      handleTemplateCreate(req.body);
      break;
    case "template.update":
      handleTemplateUpdate(req.body);
      break;
    case "template.delete":
      handleTemplateDelete(req.body);
      break;
  }

  res.json({ success: true });
});

function handleTemplateCreate(data) {
  // Save to database, send notification, etc.
  console.log("New template created:", data.template.name);
  console.log("Pages count:", data.template.pages_data?.length || 1);
}

function handleTemplateUpdate(data) {
  // Update records, sync changes, etc.
  console.log("Template updated:", data.template.name);
  console.log(
    "Canvas size:",
    `${data.template.canvas_width}x${data.template.canvas_height}`,
  );
}

function handleTemplateDelete(data) {
  // Remove from database, notify users, etc.
  console.log("Template deleted:", data.template.id);
}

PHP#

<?php
header('Content-Type: application/json');

$input = file_get_contents('php://input');
$data = json_decode($input, true);

$event_type = $data['event_type'];
$template = $data['template'];
$workspace_id = $data['workspace_id'];
$user_id = $data['user_id'];

switch ($event_type) {
    case 'template.create':
        handleTemplateCreate($data);
        break;
    case 'template.update':
        handleTemplateUpdate($data);
        break;
    case 'template.delete':
        handleTemplateDelete($data);
        break;
}

function handleTemplateDelete($data) {
    error_log("Template deleted: " . $data['template']['id']);
}

function handleTemplateCreate($data) {
    // Database insert, notification, etc.
    error_log("New template: " . $data['template']['name']);
    error_log("Template ID: " . $data['template']['id']);
}

function handleTemplateUpdate($data) {
    // Database update, sync, etc.
    error_log("Template updated: " . $data['template']['name']);
    error_log("Version: " . $data['template']['current_version']);
}

echo json_encode(['success' => true]);
?>

Python (FastAPI)#

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional, Dict, Any

app = FastAPI()

class PageData(BaseModel):
    id: str
    name: str
    thumbnail_url: Optional[str]
    canvas_data: Dict[str, Any]

class Template(BaseModel):
    id: str
    name: str
    workspace_id: str
    user_id: str
    canvas_data: Dict[str, Any]
    canvas_width: int
    canvas_height: int
    current_version: str
    thumbnail_url: Optional[str]
    description: Optional[str]
    pages_data: Optional[List[PageData]]
    created_at: str
    updated_at: str

class WebhookPayload(BaseModel):
    event_type: str
    template: Template
    workspace_id: str
    user_id: str
    timestamp: str

@app.post("/api/studio-webhook")
async def webhook_handler(payload: WebhookPayload):
    print(f"Event: {payload.event_type} for template {payload.template.id}")

    if payload.event_type == "template.create":
        await handle_template_create(payload)
    elif payload.event_type == "template.update":
        await handle_template_update(payload)
    elif payload.event_type == "template.delete":
        await handle_template_delete(payload)

    return {"success": True}

async def handle_template_create(data):
    # Process new template
    print(f"New template created: {data.template.name}")
    print(f"Canvas size: {data.template.canvas_width}x{data.template.canvas_height}")

async def handle_template_update(data):
    # Process template update
    print(f"Template updated: {data.template.name}")
    print(f"Pages count: {len(data.template.pages_data) if data.template.pages_data else 1}")

async def handle_template_delete(data):
    # Remove template from records
    print(f"Template deleted: {data.template.id}")

Common Use Cases#

Save to Database#

async function handleTemplateCreate(data) {
  await db.templates.create({
    id: data.template.id,
    name: data.template.name,
    user_id: data.user_id,
    workspace_id: data.workspace_id,
    canvas_width: data.template.canvas_width,
    canvas_height: data.template.canvas_height,
    pages_count: data.template.pages_data?.length || 1,
    thumbnail_url: data.template.thumbnail_url,
    current_version: data.template.current_version,
    created_at: data.template.created_at,
    updated_at: data.template.updated_at,
  });
}

Send Notifications#

async function handleTemplateUpdate(data) {
  // Email notification
  await sendEmail({
    to: await getUserEmail(data.user_id),
    subject: "Template Updated",
    body: `Your template "${data.template.name}" has been updated.`,
  });

  // Slack notification
  await sendSlackMessage({
    channel: "#designs",
    text: `Template "${data.template.name}" was updated by user ${data.user_id}`,
  });
}

Sync with External API#

async function handleTemplateCreate(data) {
  // Sync with external design management system
  await externalAPI.createDesign({
    id: data.template.id,
    name: data.template.name,
    creator: data.user_id,
    workspace: data.workspace_id,
    thumbnail: data.template.thumbnail_url,
    canvas_size: `${data.template.canvas_width}x${data.template.canvas_height}`,
    pages: data.template.pages_data?.length || 1,
  });
}

Security#

Verify Webhook Origin#

Always verify webhooks come from Orshot:

app.post("/api/studio-webhook", (req, res) => {
  const origin = req.get("Origin") || req.get("Referer");

  if (!origin || !origin.includes("orshot.com")) {
    return res.status(403).json({ error: "Forbidden" });
  }

  // Process webhook...
});

Use HTTPS#

Always use HTTPS endpoints for webhooks in production:

✅ https://yourapp.com/api/studio-webhook
❌ http://yourapp.com/api/studio-webhook

Validate Payload#

Check that required fields are present:

function validateWebhook(body) {
  const required = [
    "event_type",
    "template",
    "workspace_id",
    "user_id",
    "timestamp",
  ];

  for (const field of required) {
    if (!body[field]) {
      throw new Error(`Missing required field: ${field}`);
    }
  }

  if (
    !["template.create", "template.update", "template.delete"].includes(
      body.event_type,
    )
  ) {
    throw new Error(`Invalid event type: ${body.event_type}`);
  }

  // Validate template object
  const templateRequired = ["id", "name", "workspace_id", "user_id"];
  for (const field of templateRequired) {
    if (!body.template[field]) {
      throw new Error(`Missing template field: ${field}`);
    }
  }
}

Error Handling#

Retry Logic#

Handle temporary failures gracefully:

app.post("/api/studio-webhook", async (req, res) => {
  try {
    await processWebhook(req.body);
    res.json({ success: true });
  } catch (error) {
    console.error("Webhook processing failed:", error);

    // Return 5xx status for retries
    res.status(500).json({
      error: "Processing failed",
      retry: true,
    });
  }
});

Logging#

Log all webhook events for debugging:

function logWebhook(data) {
  console.log(
    JSON.stringify({
      timestamp: new Date().toISOString(),
      event_type: data.event_type,
      template_id: data.template_id,
      user_id: data.user_data?.user_id,
      success: true,
    }),
  );
}

Testing#

Local Development#

Use ngrok to expose your local server:

# Install ngrok
npm install -g ngrok

# Expose your local server
ngrok http 3000

# Use the HTTPS URL in your webhook settings
https://abc123.ngrok.io/api/studio-webhook

Test Payloads#

Create test webhook payloads for development:

const testPayload = {
  event_type: "template.create",
  template: {
    id: "test_123",
    name: "Test Template",
    workspace_id: "test_workspace",
    user_id: "test_user",
    canvas_data: {},
    canvas_width: 1080,
    canvas_height: 1080,
    current_version: "1.0",
    thumbnail_url: "https://example.com/thumb.jpg",
    description: "Test template description",
    pages_data: [
      {
        id: "page_1",
        name: "Page 1",
        thumbnail_url: "https://example.com/page1.jpg",
        canvas_data: {},
      },
    ],
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
  },
  workspace_id: "test_workspace",
  user_id: "test_user",
  timestamp: new Date().toISOString(),
};

// Send test webhook
fetch("http://localhost:3000/api/studio-webhook", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(testPayload),
});

Troubleshooting#

Webhook Not Firing#

  • Check if webhook URL is correctly configured
  • Verify the endpoint is accessible from the internet
  • Ensure you're returning a 2xx status code

Payload Issues#

  • Log the raw request body to see what's being sent
  • Check Content-Type headers
  • Verify JSON parsing is working correctly

Timeout Issues#

  • Keep webhook processing under 10 seconds
  • Use background jobs for heavy processing
  • Return success response quickly, process asynchronously

Ready to automate?

Start rendering images, PDFs and videos from your templates in under 2 minutes. Free plan, no credit card.

Get your API key
  • Image, PDF and video generation via API
  • Visual editor with AI and smart layouts
  • Zapier, Make, MCP and 50+ integrations
  • White-label embed for your own app
  • 60 free renders — no credit card required