---
title: 📬 Webhook
slug: webhook
docTags: 
createdAt: 2025-05-30T14:21:46.203Z
---

# Webhook for Masked Image Delivery

The Maskit system processes image masking requests in batches and **sends the masked images directly via webhook** calls. This document describes the webhook interface, payload format, security (HMAC signatures), and provides example implementations in multiple programming languages.

***

## 📦 Webhook Overview

- **Method**: `POST`
- **Content-Type**: `multipart/form-data`
- **Purpose**: Deliver each processed masked image directly to the client’s callback URL, without storing the image permanently.
- **Webhook called per image**: The client receives an individual webhook POST for each processed image.

***

## 📝 Request Structure

The webhook request is a `multipart/form-data` POST with the following parts:

1. **Job ID (String)**
   - **Field name:** `jobId`
   - **Type:** `text/plain`
   - **Description:** The unique identifier assigned to the job when the process-image endpoint was called. Used to track and match webhook responses with specific processing requests.
2. **Metadata (String)**
   - **Field name**: `metadata`
   - **Type**: `text/plain`
   - **Description**: Arbitrary string originally provided in the batch request, used to correlate webhook calls.
3. **Masked Image File**
   - **Field name**: `maskedImage`
   - **Type**: `image/jpeg` or `image/png`
   - **Description**: The in-memory processed masked image.
4. **Security Header**
   - **Header name**: `X-Signature`
   - **Value**: HMAC SHA-256 hex digest of the raw request body using your shared secret

***

## 📝 HMAC Signature

To ensure that incoming webhook requests are legitimate and unaltered, the Maskit system signs each request with an HMAC SHA-256 signature using a shared secret. This section explains how to validate the HMAC signature on your end to secure your integration.

- **Header**: `X-Signature`
- **Algorithm**: HMAC-SHA-256
- **Shared Secret**: `your_shared_secret`
- **How to validate**:
  1. Read the **raw request body**.
  2. Compute `HMAC_SHA256(secret, body)`.
  3. Compare it to the value in `X-Signature`.

***

## 📂 Webhook Implementation Examples

Below are example webhook receiver implementations in common programming languages. These serve as reference code snippets to help you set up your own endpoint for receiving masked images and metadata from the Maskit system.

:::CodeblockTabs
```csharp
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

namespace Webhook;

[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
    private const string SharedSecret = "your_shared_secret";

    [HttpPost]
    public async Task<IActionResult> ReceiveMaskedImageWebhook()
    {
        var signatureHeader = Request.Headers["X-Signature"].ToString();

        if (string.IsNullOrEmpty(signatureHeader))
        {
            return Unauthorized();
        }

        Request.EnableBuffering();

        using var memStream = new MemoryStream();
        await Request.Body.CopyToAsync(memStream);
        var bodyBytes = memStream.ToArray();

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SharedSecret));
        var computedHash = hmac.ComputeHash(bodyBytes);
        var computedSignature = Convert.ToHexStringLower(computedHash);

        if (!computedSignature.Equals(signatureHeader, StringComparison.InvariantCultureIgnoreCase))
        {
            return Unauthorized();
        }

        Request.Body.Position = 0;

        var form = await Request.ReadFormAsync();
        var metadata = form["metadata"].ToString();
        var jobId = form["jobId"].ToString();
        var file = form.Files.GetFile("maskedImage");

        if (file == null)
        {
            return BadRequest("Missing maskedImage file.");
        }

        Console.WriteLine($"Received image '{file.FileName}' with metadata '{metadata}' and jobId '{jobId}'");

        Directory.CreateDirectory("ReceivedImages");
        var outputPath = Path.Combine("ReceivedImages", file.FileName);
        await using var stream = file.OpenReadStream();
        await using var fileStream = System.IO.File.Create(outputPath);
        await stream.CopyToAsync(fileStream);

        return Ok();
    }
}
```

```javascript
const express = require('express');
const multer = require('multer');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const app = express();
const upload = multer();
const PORT = process.env.PORT || 3000;

const SHARED_SECRET = 'your_shared_secret';

// Middleware to capture raw body for HMAC verification
app.use('/webhook', express.raw({ type: '*/*', limit: '64mb' }));

app.post('/webhook', async (req, res) => {
  const signatureHeader = req.header('X-Signature');

  if (!signatureHeader) {
    return res.status(401).send('Missing signature');
  }

  // Compute HMAC from raw body
  const hmac = crypto.createHmac('sha256', SHARED_SECRET);
  hmac.update(req.body);
  const computedSignature = hmac.digest('hex');

  if (computedSignature.toLowerCase() !== signatureHeader.toLowerCase()) {
    return res.status(401).send('Invalid signature');
  }

  // Re-parse the body as multipart/form-data using multer
  upload.any()(req, res, (err) => {
    if (err) {
      return res.status(400).send('Invalid form data');
    }

    const metadata = req.body.metadata;
    const jobId = req.body.jobId;
    const fileField = req.files.find((f) => f.fieldname === 'maskedImage');

    if (!fileField) {
      return res.status(400).send('No maskedImage file provided');
    }

    const outputDir = path.join(__dirname, 'ReceivedImages');
    fs.mkdirSync(outputDir, { recursive: true });

    const outputPath = path.join(outputDir, fileField.originalname);
    fs.writeFileSync(outputPath, fileField.buffer);

    console.log(`Received image '${fileField.originalname}' with metadata '${metadata}' and jobId '${jobId}'`);
    return res.sendStatus(200);
  });
});

app.listen(PORT, () => {
  console.log(`Webhook server running on http://localhost:${PORT}`);
});
```

```python
from flask import Flask, request, abort
import hmac
import hashlib
import os

app = Flask(__name__)
SHARED_SECRET = b'your_shared_secret'

@app.route('/webhook', methods=['POST'])
def receive_masked_image_webhook():
    signature = request.headers.get('X-Signature')
    if not signature:
        abort(401, 'Missing signature')

    # Read raw request body for signature verification
    body = request.get_data()

    computed_hash = hmac.new(SHARED_SECRET, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(computed_hash.lower(), signature.lower()):
        abort(401, 'Invalid signature')

    if 'maskedImage' not in request.files:
        abort(400, 'No maskedImage file provided')

    file = request.files['maskedImage']
    metadata = request.form.get('metadata', '')
    job_id = request.form.get('jobId', '')

    print(f"Received image '{file.filename}' with metadata '{metadata}' and jobId '{job_id}'")

    os.makedirs('ReceivedImages', exist_ok=True)
    output_path = os.path.join('ReceivedImages', file.filename)
    file.save(output_path)

    return '', 200

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

```java
package com.example.webhook;

import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

@RestController
@RequestMapping("/webhook")
public class WebhookController {

    private static final String SHARED_SECRET = "your_shared_secret";

    @PostMapping
    public void receiveMaskedImageWebhook(
            @RequestParam("maskedImage") MultipartFile file,
            @RequestParam(value = "metadata", required = false) String metadata,
            @RequestParam(value = "jobId", required = false) String jobId,
            HttpServletRequest request
    ) {
        String signatureHeader = request.getHeader("X-Signature");
        if (signatureHeader == null || signatureHeader.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing signature");
        }

        try {
            // Read the raw body for HMAC verification
            byte[] rawBody = IOUtils.toByteArray(request.getInputStream());

            // Compute HMAC SHA256
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(SHARED_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            byte[] computedHash = mac.doFinal(rawBody);

            StringBuilder sb = new StringBuilder();
            for (byte b : computedHash) {
                sb.append(String.format("%02x", b));
            }
            String computedSignature = sb.toString();

            if (!computedSignature.equalsIgnoreCase(signatureHeader)) {
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature");
            }

            // Save the file
            if (file == null || file.isEmpty()) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "maskedImage is required");
            }

            System.out.printf("Received image '%s' with metadata '%s' and jobId '%s'%n",
                    file.getOriginalFilename(), metadata, jobId);

            File outDir = new File("ReceivedImages");
            outDir.mkdirs();
            File outFile = new File(outDir, Objects.requireNonNull(file.getOriginalFilename()));
            try (InputStream in = file.getInputStream();
                 OutputStream out = new FileOutputStream(outFile)) {
                IOUtils.copy(in, out);
            }

        } catch (Exception e) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error processing request", e);
        }
    }
}
```
:::

## 🔔 Example cURL Webhook Request

Use this `curl` command to **test your webhook implementation**. It mimics how the Maskit system will POST the masked image and metadata to your endpoint:

```bash
curl -X POST "https://client-app.com/webhook" \
  -H "Content-Type: multipart/form-data" \
  -H "X-Signature: <calculated_signature>" \
  -F "metadata=your-custom-reference-id" \
  -F "jobId=123e4567-e89b-12d3-a456-426614174000" \
  -F "maskedImage=@/path/to/masked.jpg;type=image/jpeg"
```

