close
Getting started

Integration & migration

Image & video API

Video Player SDK

DAM user guide

API overview

Account

Webhooks

Learn how to configure and receive webhooks from ImageKit securely.


ImageKit uses webhooks to notify your application when an event occurs in your account. Webhooks are particularly useful for asynchronous events such as video encoding and extension processing during uploads.

Steps to receive webhooks

  1. Configure the webhook in your ImageKit dashboard.
  2. Create a webhook endpoint as an HTTP endpoint (URL) on your server and listen for incoming webhook requests.
  3. Handle requests from ImageKit by parsing each event object and returning 2xx response status codes. You can also verify the webhook signature to ensure the authenticity of the request. We recommend using the ImageKit SDK to handle webhook signature verification and prevent replay attacks.

Configure a Webhook

Go to the Developer options in the ImageKit dashboard. Under Webhooks, you will see the list of configured webhook endpoints.

Click the "Add new" button to create a new webhook endpoint.

Enter a valid HTTP(S) endpoint, select the events you want to receive, and click "Create."

You should now see the webhook endpoint in the list.

To update an existing endpoint, click .... You can change its current status (enabled or disabled) and update the list of active events.

Listen to Webhook

Use a tool like Ngrok to make your webhook endpoint publicly accessible for testing webhook implementation.

All webhook bodies are JSON-encoded. The body schema may differ based on the event type, but the following fields are standard:

FieldDataTypeDescription
typestringType of event.
idstringUnique identifier of the event.
created_atstringTimestamp of the event in ISO8601 format.
dataJSONActual event payload in JSON format.

Verify webhook signature

Webhook endpoints are publicly accessible, so filtering out malicious requests is essential. We recommend using the webhook signature to verify the authenticity of the webhook request and payload.

ImageKit follows the Standard Webhooks specification for secure webhook verification and sends webhook-id, webhook-timestamp, and webhook-signature HMAC-SHA256 signature headers.

You can use the webhook secret, which starts with the whsec_ prefix, to verify the webhook payload as explained below.

We will continue to send the legacy x-ik-signature header as well if you are using an old SDK or old verification method.

Verify signature with ImageKit SDK

We recommend using our official libraries to verify signatures. You can perform the verification by providing the raw event payload, request headers, and the webhook secret. If verification fails, you will get an error.

For development and testing purposes, SDKs also provide methods that skip signature verification entirely - however, these should never be used in production environments as they bypass critical security measures.

We are updating our SDKs, so if you don't see a snippet for your language, refer to the manual verification process.

Copy
Copy
import ImageKit from '@imagekit/nodejs';
import express from 'express';

const client = new ImageKit({
  privateKey: "your_private_key",
  webhookSecret: "whsec_..." // Copy from ImageKit dashboard
});

const app = express();

// For mixed APIs: Use JSON parser for all non-webhook routes
app.use((req, res, next) => {
  if (req.originalUrl === '/webhook') {
    next(); // Skip JSON parsing for webhook
  } else {
    express.json()(req, res, next);
  }
});

app.post(
  '/webhook',
  // ImageKit requires the raw body to construct the event
  express.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      // Verify signature and parse webhook payload securely
      const event = client.webhooks.unwrap(req.body, {
        headers: req.headers
        // Optional: Use a different webhook secret for this request
        // key: 'whsec_different_secret'
      });
      
      // Alternative: For development/testing only (NEVER use in production)
      // const event = client.webhooks.unsafeUnwrap(req.body);

      console.log('Verified webhook event:', event.type);

      // Handle different event types with full type safety
      switch (event.type) {
        case 'video.transformation.accepted':
          console.log('Video transformation accepted:', event.data.asset.url);
          // Debugging: Track transformation requests
          break;
          
        case 'video.transformation.ready':
          console.log('Video transformation ready:', event.data.transformation.output?.url);
          // Update your database/CMS to show the transformed video
          // Example: updateVideoStatus(event.data.transformation.output.url, 'ready')
          break;
          
        case 'video.transformation.error':
          console.error('Video transformation error:', event.data.transformation.error?.reason);
          // Log error and check your origin/URL endpoint settings
          break;
          
        case 'upload.pre-transform.success':
          console.log('Pre-transform success:', event.data.fileId);
          // File uploaded and pre-transformation completed
          break;
          
        case 'upload.pre-transform.error':
          console.error('Pre-transform error:', event.data.transformation.error?.reason);
          break;
          
        case 'upload.post-transform.success':
          console.log('Post-transform success:', event.data.url);
          // Additional transformation completed
          break;
          
        case 'upload.post-transform.error':
          console.error('Post-transform error:', event.data.transformation.error?.reason);
          break;

        case 'file.created':
          console.log('DAM file created:', event.data.fileId);
          break;

        case 'file.updated':
          console.log('DAM file updated:', event.data.fileId);
          break;

        case 'file.deleted':
          console.log('DAM file deleted:', event.data.fileId);
          break;

        case 'file-version.created':
          console.log('DAM file version created:', event.data.fileId);
          break;

        case 'file-version.deleted':
          console.log('DAM file version deleted:', event.data.fileId);
          break;

        default:
          console.log(`Unhandled event type: ${event.type}`);
      }

      // Return a response to acknowledge receipt of the event
      res.json({ received: true });
      
    } catch (error) {
      // On error, log and return the error message
      console.log(`Error message: ${error.message}`);
      res.status(400).send(`Webhook Error: ${error.message}`);
    }
  }
);

const server = app.listen(3000);
console.log(
  `🚀 Webhook endpoint available at http://localhost:3000/webhook`
);
Copy
import os
from flask import Flask, request, jsonify
from imagekitio import ImageKit

app = Flask(__name__)

imagekit = ImageKit(
    private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"),
    webhook_secret="whsec_..."  # Copy from ImageKit dashboard
)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.get_data(as_text=True)
    headers = dict(request.headers)
    
    try:
        # Verify signature and parse webhook payload securely
        event = imagekit.webhooks.unwrap(
            payload=payload,
            headers=headers
            # Optional: Use a different webhook secret for this request
            # key="whsec_different_secret"
        )
        
        # Alternative: For development/testing only (NEVER use in production)
        # event = imagekit.webhooks.unsafe_unwrap(payload)
        
        print(f"Verified webhook event: {event.type}")
        
        # Handle different event types with full type safety
        if event.type == 'video.transformation.accepted':
            print(f"Video transformation accepted: {event.data.asset.url}")
            # Debugging: Track transformation requests
            
        elif event.type == 'video.transformation.ready':
            print(f"Video transformation ready: {event.data.transformation.output.url}")
            # Update your database/CMS to show the transformed video
            # Example: update_video_status(event.data.transformation.output.url, 'ready')
            
        elif event.type == 'video.transformation.error':
            print(f"Video transformation error: {event.data.transformation.error.reason}")
            # Log error and check your origin/URL endpoint settings
            
        elif event.type == 'upload.pre-transform.success':
            print(f"Pre-transform success: {event.data.file_id}")
            # File uploaded and pre-transformation completed
            
        elif event.type == 'upload.pre-transform.error':
            print(f"Pre-transform error: {event.data.transformation.error.reason}")
            
        elif event.type == 'upload.post-transform.success':
            print(f"Post-transform success: {event.data.url}")
            # Additional transformation completed
            
        elif event.type == 'upload.post-transform.error':
            print(f"Post-transform error: {event.data.transformation.error.reason}")

        elif event.type == 'file.created':
            print(f"DAM file created: {event.data.file_id}")

        elif event.type == 'file.updated':
            print(f"DAM file updated: {event.data.file_id}")

        elif event.type == 'file.deleted':
            print(f"DAM file deleted: {event.data.file_id}")

        elif event.type == 'file-version.created':
            print(f"DAM file version created: {event.data.file_id}")
        
        elif event.type == 'file-version.deleted':
            print(f"DAM file version deleted: {event.data.file_id}")
            
        else:
            print(f"Unhandled event type: {event.type}")
        
        # Return a response to acknowledge receipt of the event
        return jsonify({"received": True})
        
    except Exception as error:
        # On error, log and return the error message
        print(f"Error message: {error}")
        return jsonify({"error": f"Webhook Error: {error}"}), 400

if __name__ == '__main__':
    app.run(port=3000)
    print("🚀 Webhook endpoint available at http://localhost:3000/webhook")
Copy
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	"github.com/imagekit-developer/imagekit-go/v2"
	"github.com/imagekit-developer/imagekit-go/v2/option"
)

func main() {
	client := imagekit.NewClient(
		option.WithPrivateKey("your_private_key"),
		option.WithWebhookSecret("whsec_..."), // Copy from ImageKit dashboard
	)

	// Webhook handler with proper request body handling
	http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
		// Limit request body size to prevent abuse (64KB should be sufficient for most webhooks)
		const MaxBodyBytes = int64(65536)
		req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)
		
		// Read the raw webhook payload
		payload, err := io.ReadAll(req.Body)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
			w.WriteHeader(http.StatusServiceUnavailable)
			return
		}

		// Verify and unwrap webhook payload
		event, err := client.Webhooks.Unwrap(payload, req.Header)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Invalid webhook signature or malformed payload: %v\n", err)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		fmt.Printf("Verified webhook event: %s\n", event.Type)
		
		// Handle different event types with full type safety
		switch event.Type {
		case "video.transformation.accepted":
			videoEvent := event.AsVideoTransformationAcceptedEvent()
			fmt.Printf("Video transformation accepted: %s\n", videoEvent.Data.Asset.URL)
			// Debugging: Track transformation requests
			// handleVideoTransformationAccepted(videoEvent)
			
		case "video.transformation.ready":
			videoEvent := event.AsVideoTransformationReadyEvent()
			fmt.Printf("Video transformation ready: %s\n", videoEvent.Data.Transformation.Output.URL)
			// Update your database/CMS to show the transformed video
			// handleVideoTransformationReady(videoEvent)
			
		case "video.transformation.error":
			videoEvent := event.AsVideoTransformationErrorEvent()
			fmt.Printf("Video transformation error: %s\n", videoEvent.Data.Transformation.Error.Reason)
			// Log error and check your origin/URL endpoint settings
			// handleVideoTransformationError(videoEvent)
			
		case "upload.pre-transform.success":
			uploadEvent := event.AsUploadPreTransformSuccessEvent()
			fmt.Printf("Pre-transform success: %s\n", uploadEvent.Data.FileID)
			// File uploaded and pre-transformation completed
			// handleUploadPreTransformSuccess(uploadEvent)
			
		case "upload.post-transform.success":
			postEvent := event.AsUploadPostTransformSuccessEvent()
			fmt.Printf("Post-transform success: %s\n", postEvent.Data.Name)
			// Additional transformation completed
			// handleUploadPostTransformSuccess(postEvent)

		case "upload.pre-transform.error":
			uploadEvent := event.AsUploadPreTransformErrorEvent()
			fmt.Printf("Pre-transform error: %s\n", uploadEvent.Data.Transformation.Error.Reason)
			// Log error and investigate the transformation request

		case "upload.post-transform.error":
			postEvent := event.AsUploadPostTransformErrorEvent()
			fmt.Printf("Post-transform error: %s\n", postEvent.Data.Transformation.Error.Reason)

		case "file.created":
			fileEvent := event.AsFileCreateEvent()
			fmt.Printf("DAM file created: %s\n", fileEvent.Data.FileID)

		case "file.updated":
			fileEvent := event.AsFileUpdateEvent()
			fmt.Printf("DAM file updated: %s\n", fileEvent.Data.FileID)

		case "file.deleted":
			fileEvent := event.AsFileDeleteEvent()
			fmt.Printf("DAM file deleted: %s\n", fileEvent.Data.FileID)

		case "file-version.created":
			versionEvent := event.AsFileVersionCreateEvent()
			fmt.Printf("DAM file version created: %s\n", versionEvent.Data.FileID)

		case "file-version.deleted":
			versionEvent := event.AsFileVersionDeleteEvent()
			fmt.Printf("DAM file version deleted: %s\n", versionEvent.Data.FileID)
		// Handle other event types as needed
		default:
			fmt.Printf("Unhandled event type: %s\n", event.Type)
		}

		w.WriteHeader(http.StatusOK)
	})

	// Start the server
	fmt.Println("Webhook server listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
Copy
require 'json'
require 'sinatra'
require 'imagekitio'

client = Imagekitio::Client.new(
  private_key: "your_private_key",
  webhook_secret: "whsec_..." # Copy from ImageKit dashboard
)

# Using Sinatra
post '/webhook' do
  payload = request.body.read
  event = nil

  begin
    # Verify signature and parse webhook payload securely
    event = client.webhooks.unwrap(payload, headers: {
      "webhook-id" => request.env["HTTP_WEBHOOK_ID"],
      "webhook-timestamp" => request.env["HTTP_WEBHOOK_TIMESTAMP"],
      "webhook-signature" => request.env["HTTP_WEBHOOK_SIGNATURE"]
    })
  rescue => e
    puts "⚠️  Webhook signature verification failed. #{e.message}"
    status 400
    return
  end

  # Alternative: For development/testing only (NEVER use in production)
  # event = client.webhooks.unsafe_unwrap(payload)

  # Handle the event
  case event.type
  when 'video.transformation.accepted'
    puts "Video transformation accepted: #{event.data.asset.url}"
    # Debugging: Track transformation requests

  when 'video.transformation.ready'
    puts "Video transformation ready: #{event.data.transformation.output.url}"
    # Update your database/CMS to show the transformed video

  when 'video.transformation.error'
    puts "Video transformation error: #{event.data.transformation.error.reason}"
    # Log error and check your origin/URL endpoint settings

  when 'upload.pre-transform.success'
    puts "Pre-transform success: #{event.data.file_id}"
    # File uploaded and pre-transformation completed

  when 'upload.pre-transform.error'
    puts "Pre-transform error: #{event.data.transformation.error.reason}"

  when 'upload.post-transform.success'
    puts "Post-transform success: #{event.data.url}"
    # Additional transformation completed

  when 'upload.post-transform.error'
    puts "Post-transform error: #{event.data.transformation.error.reason}"

  when 'file.created'
    puts "DAM file created: #{event.data.file_id}"

  when 'file.updated'
    puts "DAM file updated: #{event.data.file_id}"

  when 'file.deleted'
    puts "DAM file deleted: #{event.data.file_id}"

  when 'file-version.created'
    puts "DAM file version created: #{event.data.file_id}"

  when 'file-version.deleted'
    puts "DAM file version deleted: #{event.data.file_id}"

  else
    puts "Unhandled event type: #{event.type}"
  end

  status 200
end
Copy
import io.imagekit.client.ImageKitClient;
import io.imagekit.client.okhttp.ImageKitOkHttpClient;
import io.imagekit.models.webhooks.UnwrapWebhookEvent;
import io.imagekit.models.webhooks.UnwrapWebhookParams;
import io.imagekit.models.webhooks.VideoTransformationAcceptedEvent;
import io.imagekit.models.webhooks.VideoTransformationReadyEvent;
import io.imagekit.models.webhooks.VideoTransformationErrorEvent;
import io.imagekit.models.webhooks.UploadPreTransformSuccessEvent;
import io.imagekit.models.webhooks.UploadPreTransformErrorEvent;
import io.imagekit.models.webhooks.UploadPostTransformSuccessEvent;
import io.imagekit.models.webhooks.UploadPostTransformErrorEvent;
import io.imagekit.models.webhooks.FileCreateEvent;
import io.imagekit.models.webhooks.FileUpdateEvent;
import io.imagekit.models.webhooks.FileDeleteEvent;
import io.imagekit.models.webhooks.FileVersionCreateEvent;
import io.imagekit.models.webhooks.FileVersionDeleteEvent;
import io.imagekit.core.http.Headers;
import spark.Request;
import spark.Response;
import java.util.Map;
import static spark.Spark.*;

public class WebhookHandler {
    public static void main(String[] args) {
        ImageKitClient client = ImageKitOkHttpClient.builder()
            .privateKey("your_private_key")
            .webhookSecret("whsec_...") // Copy from ImageKit dashboard
            .build();

        // Using Spark framework (http://sparkjava.com)
        post("/webhook", (request, response) -> handleWebhook(client, request, response));
        
        System.out.println("🚀 Webhook endpoint available at http://localhost:4567/webhook");
    }

    public static Object handleWebhook(ImageKitClient client, Request request, Response response) {
        String payload = request.body();
        UnwrapWebhookEvent event = null;

        try {
            // Build headers map for webhook verification
            Headers headers = Headers.builder()
                .putAll(Map.of(
                    "webhook-id", request.headers("webhook-id"),
                    "webhook-timestamp", request.headers("webhook-timestamp"),
                    "webhook-signature", request.headers("webhook-signature")
                ))
                .build();

            // Verify signature and parse webhook payload securely
            event = client.webhooks().unwrap(
                UnwrapWebhookParams.builder()
                    .payload(payload)
                    .headers(headers)
                    .build()
            );

            // Alternative: For development/testing only (NEVER use in production)
            // event = client.webhooks().unsafeUnwrap(payload);

            // Handle different event types
            if (event.isVideoTransformationAccepted()) {
                VideoTransformationAcceptedEvent videoEvent = event.asVideoTransformationAccepted();
                System.out.println("Video transformation accepted: " + videoEvent.data().asset().url());
                // Debugging: Track transformation requests
                
            } else if (event.isVideoTransformationReady()) {
                VideoTransformationReadyEvent videoEvent = event.asVideoTransformationReady();
                System.out.println("Video transformation ready: " + 
                    videoEvent.data().transformation().output().url());
                // Update your database/CMS to show the transformed video
                
            } else if (event.isVideoTransformationError()) {
                VideoTransformationErrorEvent videoEvent = event.asVideoTransformationError();
                System.out.println("Video transformation error: " + 
                    videoEvent.data().transformation().error().reason());
                // Log error and check your origin/URL endpoint settings
                
            } else if (event.isUploadPreTransformSuccess()) {
                UploadPreTransformSuccessEvent uploadEvent = event.asUploadPreTransformSuccess();
                System.out.println("Pre-transform success: " + uploadEvent.data().fileId());
                // File uploaded and pre-transformation completed
                
            } else if (event.isUploadPreTransformError()) {
                UploadPreTransformErrorEvent uploadEvent = event.asUploadPreTransformError();
                System.out.println("Pre-transform error: " + 
                    uploadEvent.data().transformation().error().reason());
                
            } else if (event.isUploadPostTransformSuccess()) {
                UploadPostTransformSuccessEvent postEvent = event.asUploadPostTransformSuccess();
                System.out.println("Post-transform success: " + postEvent.data().url());
                // Additional transformation completed
                
            } else if (event.isUploadPostTransformError()) {
                UploadPostTransformErrorEvent postEvent = event.asUploadPostTransformError();
                System.out.println("Post-transform error: " + 
                    postEvent.data().transformation().error().reason());

            } else if (event.isFileCreate()) {
                FileCreateEvent fileEvent = event.asFileCreate();
                System.out.println("DAM file created: " + fileEvent.data().fileId());

            } else if (event.isFileUpdate()) {
                FileUpdateEvent fileEvent = event.asFileUpdate();
                System.out.println("DAM file updated: " + fileEvent.data().fileId());

            } else if (event.isFileDelete()) {
                FileDeleteEvent fileEvent = event.asFileDelete();
                System.out.println("DAM file deleted: " + fileEvent.data().fileId());

            } else if (event.isFileVersionCreate()) {
                FileVersionCreateEvent versionEvent = event.asFileVersionCreate();
                System.out.println("DAM file version created: " + versionEvent.data().fileId());

            } else if (event.isFileVersionDelete()) {
                FileVersionDeleteEvent versionEvent = event.asFileVersionDelete();
                System.out.println("DAM file version deleted: " + versionEvent.data().fileId());
                
            } else {
                System.out.println("Unhandled event type");
            }

            // Return a response to acknowledge receipt of the event
            response.status(200);
            return "";

        } catch (Exception e) {
            // On error, log and return the error message
            System.out.println("⚠️  Webhook error while validating signature: " + e.getMessage());
            response.status(400);
            return "";
        }
    }
}

Verify signature manually

ImageKit follows the Standard Webhooks specification for secure webhook verification. This approach provides better security and standardized implementation.

For implementation examples in Python, Node.js (use our SDK), PHP, Go, and other languages, refer to the Standard Webhooks GitHub repository. The repository contains complete examples and libraries for all supported languages.

Important: When using Standard Webhooks libraries directly, make sure to base64 encode your webhook secret before passing it to the library.

Preventing replay attacks

When an attacker intercepts a webhook request, they can replay it multiple times with a valid signature.

To mitigate this, the ImageKit webhook signature contains a timestamp. The timestamp is generated before the webhook request is sent to your server.

The verification method in the ImageKit SDK returns the timestamp and parsed event object.

If the timestamp is within the tolerance limit, the request can be considered valid, or you can reject it.

Optionally, a stronger approach is to use a nonce to prevent replay attacks. You can use the Event ID as a nonce, which is guaranteed to be unique across all events. You can find the event ID in the id field of the event object.

List of events

Below is the list of events for which ImageKit calls your configured webhook: