Guide to Middesk Webhooks: Features and Best Practices
Middesk webhooks tell your systems when a business verification moves forward, when monitoring finds something new about a business you have onboarded, and when an agent or tax registration changes. Most Middesk work is asynchronous, so if you run KYB on Middesk, webhooks are how status changes, new liens and registration updates reach your onboarding and risk systems without polling.
This guide covers how Middesk webhooks are registered and signed, the event types and the Event envelope, the payload controls that change what arrives, and the best practices for running a Middesk receiver in production.
What are Middesk webhooks?
A Middesk webhook is an HTTP POST to a URL you register, with an Event object as the JSON body. The Event names its type, such as business.updated, and carries the affected object (a business, order, lien or run) under data.object.
Middesk offers three ways to authenticate deliveries: an HMAC signature, mutual TLS, and OAuth access tokens issued by your own identity provider. This guide focuses on the HMAC signature, which is the mode Hookdeck's Middesk source type verifies. The signature arrives in X-Middesk-Signature-256 as the lowercase hex HMAC-SHA256 of the raw request body, keyed with a secret you define when you create the webhook.
The secret is used whole. Middesk's API reference shows an example secret of whsec_abcdef123456, which looks like a Standard Webhooks key, but Middesk does not strip the prefix or base64-decode anything. The full string is the HMAC key.
Middesk webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard (Settings > Webhooks) or the Webhooks API (POST /v1/webhooks) |
| Environments | Separate endpoints for sandbox and production |
| Subscription | enabled_events per endpoint, or ["*"] for all |
| Signature header | X-Middesk-Signature-256 |
| Scheme | HMAC-SHA256, lowercase hex, no prefix |
| Signed content | The raw request body, nothing else |
| Secret | The secret you set on the webhook, used whole as the key |
| Other authentication | Mutual TLS or OAuth bearer tokens, instead of the HMAC |
| Correlation header | X-Correlation-Id: the object's external_id if set, otherwise its ID |
| Payload controls | include to choose business associations, and event schemas to reshape data.object |
| Success response | Any 2xx |
| Retries | Up to 10 times over about 3 days, with exponential backoff |
| Source IPs | 35.239.59.102, 35.192.63.74, 104.198.38.1 |
Common events
Middesk documents 40 event types across its products.
| Area | Events | Fires when |
|---|---|---|
| Business verification | business.created, business.updated | A business is created, or its status changes (except when it moves to in_audit) |
| Orders | order.created, order.updated | An order is created for a business, or updated |
| Monitoring | monitor.created, monitor.updated | A monitor is created for a business, or updated |
| Monitoring findings | lien.found, lien.terminated, lien.continued, lien.lapsed, lien.updated, bankruptcy.created, registration.created, registration.updated, address.created, name.created, person.created, watchlist_result.created and their deleted counterparts | Ongoing monitoring discovers a change to a business |
| TIN | tin.retried, tin.retrieved | A TIN retry completes, or a previously unknown TIN is retrieved |
| Industry | industry_, industry_ | An industry classification is created or completes |
| Agents | run.created, run.completed, run.interrupted | An agent run starts, finishes, or pauses for input |
| Entity management | agent_tax_registration.*, registration_request.*, agent_, agent_info_request.*, agent_item_change.*, agent_mail.available | Tax and jurisdiction registrations, information requests and mail change |
Every event uses the same envelope. Here is Middesk's default business.created payload, trimmed:
{
"object": "event",
"id": "f215a707-655e-400f-84e6-fbb949f5612a",
"type": "business.created",
"data": {
"object": {
"object": "business",
"id": "0f86dab5-8195-4b95-b3c0-19deaeba2a8e",
"name": "A Company",
"status": "open",
"external_id": null,
"orders": [
{
"id": "d9e4076c-25d7-4a93-917b-66568459cbc4",
"object": "order",
"status": "pending",
"product": "identity"
}
],
"registrations": [],
"addresses": [],
"created_at": "2020-01-02T23:54:48.154Z"
}
},
"created_at": "2020-01-02T23:54:48.239Z"
}
object is always event, id is unique per event, and type is what you route on. data.object can be large for business events, because by default it includes every association: registrations, people, addresses, TIN, watchlist, orders and more. Entity-management events also carry an account_id.
Setting up Middesk webhooks
Choose a strong random secret first, for example with openssl rand -hex 32, and store it server-side as MIDDESK_WEBHOOK_SECRET. Then register the endpoint, either in the Dashboard under Settings > Webhooks or through the API:
curl https://api.middesk.com/v1/webhooks \
-H "Authorization: Bearer $MIDDESK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/middesk",
"enabled_events": ["business.created", "business.updated", "order.updated"],
"secret": "'"$MIDDESK_WEBHOOK_SECRET"'"
}'
Middesk's signature docs say to define the secret through the Webhooks API. Create one endpoint for sandbox and another for production, each with its own secret.
Two optional fields shape the payload:
includetakes a comma-separated list of business associations, such as"tin,formation". Only those associations appear, and scalar fields likeid,nameandstatusare always present."identifiers"sends only the top-level scalars. Filtering applies tobusiness.*events only.- Event schemas are GraphQL selection sets per event type, for example
{ id name status runs { id agent } }forbusiness.updated. They replacedata.objectwith exactly the fields you select, while the envelope stays the same.
If you receive Middesk webhooks through Hookdeck, paste the whole secret exactly as you set it on the Middesk webhook, including any whsec_ prefix. Middesk uses it as a plain HMAC key, and so does Hookdeck.
Securing Middesk webhooks
Read X-Middesk-Signature-256, compute the hex HMAC-SHA256 of the raw request body with your secret as the key, and compare the two in constant time. Do not use a Standard Webhooks or Svix library: there are no webhook-id or webhook-timestamp headers, and those libraries would decode the secret.
const crypto = require('crypto');
const express = require('express');
function verifyMiddeskSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
// The whole secret string is the key, including any "whsec_" prefix.
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/middesk', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyMiddeskSignature(req.body, req.get('x-middesk-signature-256'), process.env.MIDDESK_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
// Route on event.type, deduplicate on event.id, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_middesk_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_middesk_signature(raw_body, request.headers.get("x-middesk-signature-256"),
# os.environ["MIDDESK_WEBHOOK_SECRET"])
Middesk's Ruby and Python samples return 400 on a bad signature and its JavaScript sample returns 401. Either works, since Middesk retries any non-2xx. Middesk's JavaScript sample also compares with !==; a timing-safe comparison, as above, is the better choice.
The signature covers only the body and Middesk sends no timestamp, so there is no replay window to check. If you also want to restrict the network path, Middesk publishes its three source IPs, and mutual TLS lets you require Middesk's client certificate during the handshake.
Make Middesk webhooks production-ready. Hookdeck Event Gateway verifies the
X-Middesk-Signature-256header with your whole secret, stores every verification and monitoring event, and retries delivery on your schedule rather than Middesk's.
Middesk webhook limitations and pain points
A secret that looks like Standard Webhooks but is not
The Problem: Every delivery fails verification, even though the secret is right.
Why It Happens: Middesk's examples use secrets like whsec_abcdef123456. Developers who recognise the prefix reach for a Standard Webhooks library, or strip whsec_ and base64-decode the rest. Middesk does neither, so the computed key is different.
Workarounds:
- Use the full secret string as the HMAC key.
- Verify manually with your language's HMAC library, as shown above.
- Check you are using the sandbox secret for sandbox events and the production secret for production events.
How Hookdeck Can Help: Hookdeck's Middesk source uses the secret as entered, with no decoding, and verifies every request in one place.
Payload shape depends on configuration nobody can see
The Problem: Two business.updated events from two endpoints have different shapes, and code written against one breaks on the other.
Why It Happens: include and event schemas change data.object per endpoint and per event type, and nothing in the delivery says which configuration applied.
Workarounds:
- Keep one endpoint per consumer, with its
includeand schemas recorded alongside the consumer's code. - Treat every association as optional, and fetch the business from the API when you need a field that is missing.
How Hookdeck Can Help: Hookdeck stores the full body of every request, so you can see exactly which fields each endpoint receives and replay real examples against a handler before you change its configuration.
No replay protection
The Problem: A captured request still verifies if someone sends it again later.
Why It Happens: Middesk signs only the body and sends no timestamp.
Workarounds:
- Deduplicate on the Event
id, which you need anyway because of retries. - Restrict access to Middesk's published IPs, or use mutual TLS.
How Hookdeck Can Help: Hookdeck's deduplication can drop requests whose id it has already seen before they reach your service.
Three days of retries, then nothing
The Problem: After a long outage, events you never processed are gone from Middesk's retry queue.
Why It Happens: Middesk retries a failed delivery up to 10 times, the first after 1 to 30 seconds and then with exponential backoff, with the last attempt about 3 days after the first. After that it stops.
Workarounds:
- Return a
2xxas soon as the request is verified and stored. - Reconcile pending businesses through the API after any outage longer than a day or two.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, so Middesk's retries are not used up, and then delivers to your service with retries you configure and replay for anything that still fails.
Best practices
Verify against the raw body
Middesk warns that parsing the body before verifying it can change the bytes. In Express, use express.raw({ type: '*/*' }). In Next.js, use await request.text(). In FastAPI, use await request.body().
Route on type, not object
object is always event. Switch on type, and log and acknowledge any type you do not recognise.
Use X-Correlation-Id to match your records
Set external_id on businesses you create, and Middesk returns it in the X-Correlation-Id header. You can look up your own record without parsing a large body.
Trim business payloads
Business events include every association by default. If you only need the status, set include to "identifiers" and fetch details from the API when you need them.
Acknowledge fast and process asynchronously
Verify, store the event, return a 2xx, and run the onboarding or risk logic afterwards. See why to process webhooks asynchronously.
Make handlers idempotent
Retries can repeat an event. Store the Event id before processing and skip any you have already handled. See our guide to webhook idempotency.
Conclusion
Middesk signs webhooks with a lowercase hex HMAC-SHA256 of the raw body in X-Middesk-Signature-256, keyed with the whole secret you set, even when that secret starts with whsec_. Verify against the raw bytes, route on type, deduplicate on the Event id, and keep an eye on include and event schemas, because they change what each endpoint receives.
Hookdeck Event Gateway verifies every Middesk signature, stores each event, and redelivers on your schedule, so an outage on your side does not run out Middesk's three-day retry window.
Get started with Hookdeck for free and handle Middesk webhooks reliably in minutes.