🔌API & Integration
📬 Webhook
8 min
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 metadata (string) field name metadata type text/plain description arbitrary string originally provided in the batch request, used to correlate webhook calls masked image file field name maskedimage type image/jpeg or image/png description the in memory processed masked image 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 read the raw request body compute hmac sha256(secret, body) 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 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 file = form files getfile("maskedimage"); if (file == null) { return badrequest(); } console writeline($"received image '{file filename}' with metadata '{metadata}'"); 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(); } }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 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}'`); return res sendstatus(200); }); }); app listen(port, () => { console log(`webhook server running on http //localhost ${port}`); }); 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', '') print(f"received image '{file filename}' with metadata '{metadata}'") 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) package com example webhook; import jakarta servlet http httpservletrequest; import org apache commons io ioutils; import org springframework http httpstatus; import org springframework util digestutils; import org springframework util streamutils; 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 security messagedigest; 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, 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'%n", file getoriginalfilename(), metadata); 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 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 "maskedimage=@/path/to/masked jpg;type=image/jpeg"