Webhook Documentation

Receive real-time notifications when your video submissions complete

Quick Navigation

Overview

Webhooks allow you to receive real-time HTTP notifications when events occur in your AdMorph account. Instead of continuously polling the API to check if a video is ready, AdMorph will send a POST request to your configured webhook URL when submissions complete or fail.

Benefits

Setting Up Webhooks

Step 1: Configure Your Webhook URL

  1. Log in to your AdMorph dashboard
  2. Navigate to Settings → Webhook Configuration
  3. Click "Configure Webhook"
  4. Enter your webhook endpoint URL (must be HTTPS in production)
  5. Save the webhook secret - you'll need this to verify webhook signatures

Important: Your webhook secret is shown only once. Store it securely - you'll need it to verify that webhooks are genuinely from AdMorph.

Step 2: Implement Your Webhook Endpoint

Your webhook endpoint should:

Tip: For local development, use tools like ngrok or localtunnel to expose your local server to the internet.

Event Types

AdMorph sends webhooks for the following events:

Event Description When Sent
submission.completed Video generation completed successfully When all videos for a submission are ready
submission.failed Video generation failed When a submission encounters an error
submission.started Video generation started When processing begins (optional)

Payload Structure

All webhook events follow this structure:

{
  "event": "submission.completed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "submissionId": "507f1f77bcf86cd799439011",
    "submissionType": "sora2",
    "status": "completed",
    "creditsUsed": 1,
    "videos": [
      "https://admorph.net/videos/output_123.mp4"
    ],
    "duration": "10",
    "orientation": "16:9",
    "createdAt": "2025-01-15T10:25:00.000Z",
    "completedAt": "2025-01-15T10:30:00.000Z"
  }
}

submission.completed Payload

{
  "event": "submission.completed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "submissionId": "507f1f77bcf86cd799439011",
    "submissionType": "ugc-pro",
    "status": "completed",
    "creditsUsed": 3,
    "videos": [
      "https://admorph.net/videos/ugc_123_v1.mp4",
      "https://admorph.net/videos/ugc_123_v2.mp4",
      "https://admorph.net/videos/ugc_123_v3.mp4"
    ],
    "duration": null,
    "orientation": null,
    "createdAt": "2025-01-15T10:20:00.000Z",
    "completedAt": "2025-01-15T10:30:00.000Z"
  }
}

submission.failed Payload

{
  "event": "submission.failed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "submissionId": "507f1f77bcf86cd799439011",
    "submissionType": "sora2",
    "status": "failed",
    "creditsReserved": 1,
    "createdAt": "2025-01-15T10:25:00.000Z",
    "failedAt": "2025-01-15T10:30:00.000Z"
  }
}

Security & Signature Verification

AdMorph signs all webhook requests using HMAC-SHA256. This allows you to verify that webhooks are genuinely from AdMorph and haven't been tampered with.

Webhook Headers

X-AdMorph-Signature: sha256=1a2b3c4d5e6f7g8h9i0j...
X-AdMorph-Event: submission.completed
User-Agent: AdMorph-Webhook/1.0
Content-Type: application/json

Verifying the Signature

Python

import hmac
import hashlib

def verify_webhook_signature(payload, signature, secret):
    """Verify AdMorph webhook signature"""
    # Compute expected signature
    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Compare using timing-safe comparison
    return hmac.compare_digest(expected, signature)

# In your Flask/Django webhook handler
@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-AdMorph-Signature')
    secret = 'your_webhook_secret_here'

    if not verify_webhook_signature(payload, signature, secret):
        return 'Invalid signature', 401

    # Process the webhook
    event = request.json
    print(f"Received event: {event['event']}")

    return 'OK', 200

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
    const expectedSignature = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
    );
}

// Express.js webhook handler
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
    const payload = req.body.toString('utf8');
    const signature = req.headers['x-admorph-signature'];
    const secret = 'your_webhook_secret_here';

    if (!verifyWebhookSignature(payload, signature, secret)) {
        return res.status(401).send('Invalid signature');
    }

    // Process the webhook
    const event = JSON.parse(payload);
    console.log(`Received event: ${event.event}`);

    res.status(200).send('OK');
});

PHP

<?php

function verifyWebhookSignature($payload, $signature, $secret) {
    $expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expectedSignature, $signature);
}

// Webhook handler
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ADMORPH_SIGNATURE'];
$secret = 'your_webhook_secret_here';

if (!verifyWebhookSignature($payload, $signature, $secret)) {
    http_response_code(401);
    die('Invalid signature');
}

// Process the webhook
$event = json_decode($payload, true);
error_log("Received event: " . $event['event']);

http_response_code(200);
echo 'OK';

?>

Security Best Practice: Always verify webhook signatures in production to prevent unauthorized requests to your webhook endpoint.

Retry Logic

If your webhook endpoint doesn't respond with a 2xx status code, AdMorph will automatically retry the webhook.

Retry Behavior

Tip: Make your webhook handler idempotent so it can safely process the same event multiple times without side effects.

Best Practices

Complete Examples

Flask (Python) Webhook Handler

from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)

WEBHOOK_SECRET = 'your_webhook_secret_here'

def verify_signature(payload, signature):
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Get raw payload and signature
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-AdMorph-Signature')

    # Verify signature
    if not verify_signature(payload, signature):
        return jsonify({'error': 'Invalid signature'}), 401

    # Parse event
    event = request.json
    event_type = event['event']
    data = event['data']

    # Handle different event types
    if event_type == 'submission.completed':
        submission_id = data['submissionId']
        videos = data['videos']
        print(f"✓ Submission {submission_id} completed")
        print(f"  Videos: {videos}")

        # TODO: Add your business logic here
        # - Update database
        # - Send email notification
        # - Process videos

    elif event_type == 'submission.failed':
        submission_id = data['submissionId']
        print(f"✗ Submission {submission_id} failed")

        # TODO: Handle failure
        # - Alert user
        # - Log error

    return jsonify({'status': 'received'}), 200

if __name__ == '__main__':
    app.run(port=5000)

Express.js (Node.js) Webhook Handler

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret_here';

function verifySignature(payload, signature) {
    const expected = 'sha256=' + crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(payload)
        .digest('hex');

    try {
        return crypto.timingSafeEqual(
            Buffer.from(signature),
            Buffer.from(expected)
        );
    } catch (e) {
        return false;
    }
}

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
    // Get raw payload and signature
    const payload = req.body.toString('utf8');
    const signature = req.headers['x-admorph-signature'];

    // Verify signature
    if (!verifySignature(payload, signature)) {
        return res.status(401).json({error: 'Invalid signature'});
    }

    // Parse event
    const event = JSON.parse(payload);
    const eventType = event.event;
    const data = event.data;

    // Handle different event types
    if (eventType === 'submission.completed') {
        const submissionId = data.submissionId;
        const videos = data.videos;

        console.log(`✓ Submission ${submissionId} completed`);
        console.log(`  Videos: ${videos.join(', ')}`);

        // TODO: Add your business logic here

    } else if (eventType === 'submission.failed') {
        const submissionId = data.submissionId;

        console.log(`✗ Submission ${submissionId} failed`);

        // TODO: Handle failure
    }

    res.status(200).json({status: 'received'});
});

app.listen(3000, () => {
    console.log('Webhook server listening on port 3000');
});

Testing Webhooks

For local development, you can use these tools to expose your local server:

Example with ngrok: Run ngrok http 3000 to get a public URL like https://abc123.ngrok.io that forwards to your local server.

← Getting Started Error Reference →