> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.middesk.com/build/secure-webhooks/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.middesk.com/_mcp/server. # Secure webhooks > Authenticate webhook requests using HMAC signatures, Mutual TLS, or OAuth access tokens Middesk supports three approaches to verify that webhook notifications are authentic and originate from Middesk. ## Verify signatures Middesk signs webhook requests by including a signature in the `X-Middesk-Signature-256` header. Verify this signature to confirm the request came from Middesk. > **Warning** > > Always verify the HMAC signature against the raw request body before parsing it as JSON. Parsing can alter the byte sequence (encoding changes, JSON reformatting, whitespace trimming), causing signature verification to fail. Only parse verified payloads. ### Set up signature verification Before you can verify signatures, define a secret using the [Webhooks API](/api-reference/webhooks/create-webhook). Middesk generates signatures using HMAC with SHA-256. ### Verify the signature in your webhook endpoint 1. Extract the signature from the `X-Middesk-Signature-256` header. 2. Compute an HMAC with SHA-256 using your secret as the key and the raw request body as the message. 3. Compare the header signature to your computed signature using a constant-time comparison method. ```ruby require 'openssl' secret = 'sec_...' def verify(payload, signature) digest = OpenSSL::Digest.new('sha256') expected = OpenSSL::HMAC.hexdigest(digest, secret, payload) OpenSSL.secure_compare(expected, signature) end post '/my/webhook/url' do payload = request.body.read sig_header = request.env['X_MIDDESK_SIGNATURE_256'] unless verify(payload, sig_header) # Invalid signature status 400 return end event = JSON.parse(payload) case event.type when 'business.created' business = event.data.object puts 'Business created!' when 'business.updated' business = event.data.object puts 'Business updated!' # ... handle other event types else # Unexpected event type status 400 return end status 200 end ``` ```python from flask import Flask, request, abort import hmac import hashlib import json app = Flask(__name__) secret = 'sec_...' def verify(payload, signature): expected = hmac.new(secret, payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) @app.route('/my/webhook/url', methods=['POST']) def receive_webhook(): payload = request.data sig_header = request.headers['X-Middesk-Signature-256'] if not verify(payload, sig_header): # Invalid signature abort(400) event = json.loads(payload) if event['type'] == 'business.created': business = event['data']['object'] print('Business created!') elif event['type'] == 'business.updated': business = event['data']['object'] print('Business updated!') # ... handle other event types else: # Unexpected event type abort(400) return '' ``` ```javascript const bodyParser = require('body-parser'); const crypto = require('crypto'); const express = require('express'); const app = express(); const MIDDLEWARE_WEBHOOK_SECRET = process.env.MIDDESK_WEBHOOK_SECRET; // Route-specific middleware for raw body parsing with signature verification. const verifyMiddleware = bodyParser.json({ verify: function(req, res, buf, encoding) { const signatureHeader = req.get('X-Middesk-Signature-256'); const signature = crypto .createHmac('sha256', MIDDLEWARE_WEBHOOK_SECRET) .update(buf) .digest('hex'); if (signature !== signatureHeader) { res.status(401).send('Invalid signature.'); throw new Error(`Invalid signature. Got ${signature}. Expected ${signatureHeader}`); } } }); // Apply the verification middleware only to this specific route. app.post('/my/webhook/url', verifyMiddleware, (req, res) => { // If here, the signature is valid. var event = req.body; switch (event.type) { case 'business.created': business = event.data.object; console.log(`Business ${business.id} created!`); break; case 'business.updated': business = event.data.object; console.log(`Business ${business.id} updated!`); break; // ... handle other event types. default: res.status(400).send('Unexpecetd event type'); return; } res.status(200) }); // Start the server const port = 3000; app.listen(port, () => { console.log(`Server running on port ${port}`); }); ``` ## Use mutual TLS The second way to authenticate webhook notifications is to verify Middesk's client certificate during the TLS handshake. In standard TLS, only the server presents a certificate. With Mutual TLS, the client (Middesk) also presents a certificate. Configure your server to request and verify this client certificate during the TLS handshake. ### Verify Middesk's certificate Middesk's webhook client certificate is issued by the `DigiCert Assured ID Root G2` Certificate Authority. This CA is configured on most operating systems by default, or you can [download it directly](https://cacerts.digicert.com/DigiCertAssuredIDRootG2.crt.pem). Verify the client certificate has these Subject Distinguished Name (Subject DN) fields: ``` O=Middesk\, Inc. CN=webhooks.middesk.com ``` These fields confirm the certificate was issued to Middesk, Inc. and is used for the intended purpose. ### Configure reverse proxies If you use a reverse proxy for TLS termination, configure it to request and verify the client certificate before routing requests to your backend. Verify the certificate subject matches the fields above before forwarding the request. ## Use OAuth access tokens A third, but most advanced, mechanism to authenticate webhook notifications is to use OAuth access tokens issued by your OpenID Connect Identity Provider. This method is best for enterprises with complex security requirements. Use this method if: * **HMAC shared secrets don't meet your security requirements**. Your organization prohibits sharing secrets externally. * **Mutual TLS is incompatible with your infrastructure**. Your edge proxies don't support client certificate authentication or make it difficult to relay certificate information to backend applications. * **You need separation of security responsibilities**. Security teams manage the Identity Provider while application teams focus on token verification. * **You require precise token scoping**. Tokens can be scoped to specific resource URIs. * **You must comply with internal governance**. Your organization requires internal IdP-issued tokens for all integrations. ### How OAuth authentication works