Skip to content

RESOURCES / BLOG

Triggering Video Transformations via Webhooks and API Calls

If you’re only applying a single transformation to a short clip, manually processing may be fine. However, most post-production video workflows involve compressing, trimming, overlaying watermarks, and creating platform-specific variants. 

When each of these steps depends on a person or a scheduled task to trigger them, bottlenecks arise. Nobody knows which step is pending or next, and files get lost in the shuffle. An event-driven automation workflow eliminates bottlenecks by triggering each transformation as soon as the previous one finishes. Instead of polling for completion or manually kicking off new jobs, your pipeline responds automatically as events occur.

In this post, we’ll design an automated video processing pipeline using Cloudinary’s eager asynchronous transformations and webhook notifications, so each stage in your workflow runs smoothly.

Before we get into how these two features work together, let’s start by understanding eager asynchronous transformations.

Eager transformations let you create specific versions of a video or image as soon as the file is uploaded. You define the transformations in the upload request, and Cloudinary processes them right away instead of waiting for someone to request them later.

For example, with the Cloudinary Node.js SDK, you can immediately generate a compressed MP4 variant and a thumbnail from a single video upload, as shown below:

cloudinary.uploader.upload('sample.mp4', {

  resource_type: 'video',

  eager: [

    { quality: 'auto', bit_rate: '1200k', format: 'mp4' }, // compressed variant

    { width: 320, height: 180, crop: 'fill', gravity: 'auto', format: 'jpg' } // thumbnail

  ]

})

.then(result => console.log('Upload + eager transforms complete:', result))

.catch(console.error);Code language: JavaScript (javascript)
Note:

To try the code above, you’ll need a Cloudinary account and the Node.js SDK installed. In your project, include the SDK and make sure your environment variables (CLOUDINARY_URL or API credentials) are set up.

By default, eager transformations are synchronous. That means in the code above, Cloudinary processes the transformations during the upload call, and your application waits until they’re finished before getting a response.

This setup is for lightweight transformations that you need immediately, but it can slow things down if the work is heavier or if you’re processing multiple variants at the same time.

To avoid that slowdown, you can run eager asynchronous transformations by setting eager_async: true

cloudinary.uploader.upload('sample.mp4', {

  resource_type: 'video',

  eager: [

    { quality: 'auto', bit_rate: '1200k', format: 'mp4' },            // compressed

    { width: 320, height: 180, crop: 'fill', gravity: 'auto', format: 'jpg' } // thumbnail

  ],

  eager_async: true

})

.then(r => console.log('Upload complete; transforms running in background:', r.public_id))

.catch(console.error);Code language: JavaScript (javascript)

eager_async: true enables transformations to start right away, but they run in the background on Cloudinary’s infrastructure. Your application gets a response as soon as the file is stored, while the defined eager transformations continue processing asynchronously.

For eager transformations, you can set a callback URL to notify your application when the transformation is complete. Once Cloudinary finishes processing, it will send a webhook to the URL you specify with details about the new assets.

cloudinary.uploader.upload('sample.mp4', {

  resource_type: 'video',

  eager: [

    { quality: 'auto', bit_rate: '1200k', format: 'mp4' },

    { width: 320, height: 180, crop: 'fill', gravity: 'auto', format: 'jpg' }

  ],

  eager_async: true,

  eager_notification_url: 'https://your-app.com/webhooks/eager-complete'

})

.then(r => console.log('Waiting for webhook:', r.public_id))

.catch(console.error);Code language: JavaScript (javascript)

From there, you can automatically trigger the next step in your workflow, such as overlaying captions, running moderation, or delivering the processed files to their final destination.

Below are different scenarios in which you can apply an event-driven transformation workflow to build multi-step video processing pipelines.

Let’s say you want to build a process that compresses a video, creates a thumbnail, and then adds captions before publication.

Start by uploading the video with eager transformations for compression and thumbnail generation. Then, set the eager_notification_url so Cloudinary can send a webhook to your application when those transformations are complete:

// chain-video-transformation/index.js

const { v2: cloudinary } = require("cloudinary");

cloudinary.config({

  cloud_name: "<YOUR_CLD_CLOUD>",

  api_key: "<YOUR_CLD_KEY>",

  api_secret: "<YOUR_CLD_SECRET>",

  secure: true,

});

cloudinary.uploader.upload("sample.mp4", {

  resource_type: "video",

  eager: [

    { quality: "auto", bit_rate: "1200k", format: "mp4" },

    { width: 320, height: 180, crop: "fill", gravity: "auto", format: "jpg" },

  ],

  eager_async: true,

  eager_notification_url: "https://your-app.com/webhooks/eager-complete",

});Code language: JavaScript (javascript)

By implementing the code above, you can automate the process of uploading a video, applying compression, generating a thumbnail, and receiving a webhook notification at your-app.com/webhooks/eager-complete once all tasks are complete.

Next, you can create a webhook endpoint using Express, (lightweight web framework for Node.js) to handle the incoming notification and trigger the next step of adding captions to the compressed video.

// chain-video-transformation/server.js

const express = require("express");

const { v2: cloudinary } = require("cloudinary");

cloudinary.config({

  cloud_name: "<YOUR_CLD_CLOUD>",

  api_key: "<YOUR_CLD_KEY>",

  api_secret: "<YOUR_CLD_SECRET>",

  secure: true,

});

const app = express();

app.use(express.json());

app.post("/webhooks/eager-complete", async (req, res) => {

  const publicId = req.body.public_id;

  await cloudinary.uploader.explicit(publicId, {

    resource_type: "video",

    type: "upload",

    eager: [

      {

        overlay: {

          resource_type: "subtitles",

          public_id: "captions/myfile.vtt",

        },

        flags: "layer_apply",

        quality: "auto",

        bit_rate: "1200k",

        format: "mp4",

      },

    ],

    eager_async: true,

    eager_notification_url: "https://your-app.com/webhooks/final-video",

  });

  res.sendStatus(200);

});

app.listen(9090, () => {

  console.log(`Server is running on port http://localhost:9090`);

});Code language: PHP (php)

The webhook above listens for Cloudinary’s notification when the first set of transformations is complete, then runs an additional transformation to overlay captions on the processed video. Once that step finishes, it triggers another webhook notification to your-app.com/webhooks/final-video for final processing.

Finally, define a second webhook endpoint that pushes the fully processed video to your CMS or delivery queue:

app.post("/webhooks/final-video", (req, res) => {

  const derivedAssets = req.body.derived || [];

  const finalUrl = req.body.eager[0]?.url;

  if (finalUrl) {

    // Push finalUrl to CMS, publish to queue, etc.

  }

  res.sendStatus(200);

});Code language: PHP (php)

You can find the complete code for this example in this GitHub repository.

Suppose you need to import a large video catalog, convert each file into delivery-ready formats, and validate each video (e.g., ensuring it meets specific duration or resolution requirements) before adding it to your platform.

Start by batch-uploading each file to Cloudinary with eager asynchronous transformations that generate HD, SD, and a preview thumbnail in the background.

// bulk-ingestion-with-transcoding/index.js

const { v2: cloudinary } = require("cloudinary");

cloudinary.config({

  cloud_name: "<YOUR_CLD_CLOUD>",

  api_key: "<YOUR_CLD_KEY>",

  api_secret: "<YOUR_CLD_SECRET>",

  secure: true,

});

const sources = [

  "path/to/sample.mp4",

  "path/to/sample2.mp4",

  "path/to/sample3.mp4",

];

const eagerPresets = [

  {

    width: 1280,

    height: 720,

    crop: "limit",

    quality: "auto",

    bit_rate: "2500k",

    format: "mp4",

  }, // HD

  {

    width: 854,

    height: 480,

    crop: "limit",

    quality: "auto",

    bit_rate: "1200k",

    format: "mp4",

  }, // SD

  { width: 320, height: 180, crop: "fill", gravity: "auto", format: "jpg" }, // thumbnail

];

async function uploadBatch() {

  for (const src of sources) {

    await cloudinary.uploader.upload(src, {

      resource_type: "video",

      folder: "ingest/bulk",

      eager: eagerPresets,

      eager_async: true,

      eager_notification_url: "https://your-app.com/webhooks/bulk-eager",

    });

  }

}

uploadBatch().catch(console.error);Code language: JavaScript (javascript)

This code uploads each video in the sources list, creates three derived versions (HD, SD, and a thumbnail), and sets an eager_notification_url so Cloudinary will send a webhook to your endpoint once all transformations are complete.

To handle the webhook callback, you’ll need a small web server capable of receiving HTTP POST requests from Cloudinary, such as Express.

In the example below, you’ll define an endpoint (/webhooks/bulk-eager) that Cloudinary will call once all eager transformations are finished. Inside this handler, we’ll validate each video, tag it accordingly, and log the result.

// bulk-ingestion-with-transcoding/server.js

const express = require("express");

const { v2: cloudinary } = require("cloudinary");

cloudinary.config({

  cloud_name: "<YOUR_CLD_CLOUD>",

  api_key: "<YOUR_CLD_KEY>",

  api_secret: "<YOUR_CLD_SECRET>",

  secure: true,

});

const app = express();

const runModerationCheck = async (publicId, derived) => {

  const video = (derived || []).find((d) => d.format === "mp4");

  const duration = video?.duration || 0;

  console.log(`Moderation check for ${publicId}: duration ${duration}s`);

  return duration >= 5 && duration <= 600;

};

app.post("/webhooks/bulk-eager", async (req, res) => {

  const event = req.body;

  if (event.notification_type !== "eager" || event.status !== "success")

    return res.sendStatus(200);

  const { public_id: publicId, derived = [] } = event;

  const approved = await runModerationCheck(publicId, derived);

  if (!approved) {

    await cloudinary.uploader.add_tag(["quarantine"], [publicId], {

      resource_type: "video",

    });

    console.log("Quarantined:", publicId);

    return res.sendStatus(200);

  }

  await cloudinary.uploader.add_tag(["approved"], [publicId], {

    resource_type: "video",

  });

  console.log("Approved:", publicId);

  // do something with the approved video

  res.sendStatus(200);

});

app.listen(9090, () => {

  console.log(`Server is running on port http://localhost:9090`);

});Code language: JavaScript (javascript)

The code above defines a runModerationCheck() function that approves videos only if their duration is between 6 and 500 seconds. In the webhook handler, this check runs whenever the transformations from the previous step are complete. 

Videos that fail are tagged as quarantine; those that pass are tagged as approved. Additionally, for approved videos, the handler retrieves the HD, SD, and thumbnail URLs from the derived list and logs them, ready for publishing.

You can find the complete code on GitHub.

  1. No polling or manual triggers. Each transformation stage starts automatically as soon as the previous one finishes, so you don’t have to make constant status checks.
  2. Asynchronous background processing. Cloudinary handles all heavy compute operations behind the scenes, keeping your application responsive while assets are processed.
  3. Scalable chaining. Webhooks enable you to chain multiple transformation steps, such as compression, captioning, and publishing in a reliable, event-driven sequence.
  4. Lower latency and fewer errors. Automation removes repetitive manual actions, reducing human error and accelerating your content pipeline from upload to delivery.
  5. Improved cost and quota control. By batching and grouping transformations, you can better manage API calls and processing usage, leading to more predictable billing and performance.

These best practices can help ensure your automated video pipeline runs efficiently and at scale:

  1. Verify webhook authenticity. Always validate incoming webhooks using Cloudinary’s timestamp and signature headers before processing them. This ensures the request originated from Cloudinary and protects your endpoints from spoofed or malicious calls.
  2. Handle retries gracefully. Cloudinary automatically retries failed webhook deliveries up to three times. Make your webhook handlers idempotent and resilient, so duplicate notifications don’t cause repeated transformations or conflicting updates.
  3. Use separate endpoints for upload and eager events. Differentiate between upload completion events and eager transformation notifications by routing them to distinct webhook endpoints. This separation keeps your logic modular and prevents accidental cross-triggering of unrelated actions.
  4. Track transformation chains. Use transformation grouping or derived asset trees to visualize and manage your processing pipelines. This helps track which assets were generated from a source file, making debugging and reporting much easier in multi-step workflows.
  5. Avoid redundant transformations. Leverage caching and transformation tracking to prevent reprocessing identical variants. Before launching a new transformation, check whether an equivalent derived asset already exists to save compute resources and API quota.
  6. Monitor and measure pipeline health. Track event timings, error rates, and transformation counts to identify bottlenecks early. Monitoring how long each stage takes helps you detect slowdowns, failed callbacks, or retry loops before they affect publishing or delivery.

Automatically trigger each step the moment the last one finishes with Cloudinary’s eager asynchronous transformations and webhook notifications. By using an event-driven transformation workflow, you can build smarter video pipelines that moderates content, tailors output for different platforms, and routes assets based on business rules.

Ready to automate your video workflow? Sign up for a free Cloudinary account and build your own event-driven video pipelines with eager transformations and webhooks.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free