Webhooks
Webhooks let your server receive scan status updates automatically instead of polling GET /v1/scans/:id/status. When a scan transitions between states, we send an HTTP POST request to your registered URL.
Overview
The typical scan flow without webhooks requires polling:
- Create a scan via
POST /v1/scans - Poll
GET /v1/scans/:id/statusevery few seconds - Stop polling when
statusiscompletedorerror
With webhooks, step 2 is replaced — your server receives updates automatically as they happen.
Registering a webhook
Webhooks are configured through the Enhub dashboard. Navigate to Organization → API in your project to register, update, or delete webhook endpoints.
When creating a webhook you'll configure:
- Name
url- Type
- string
- Description
The URL where webhook events will be delivered. Must be publicly accessible and respond with a
2xxstatus code.
- Name
events- Type
- string[]
- Description
Which event types to subscribe to. See Event types below.
- Name
headers- Type
- object
- Description
Optional custom headers to include in every webhook request (e.g.
{ "X-Custom-Header": "value" }).
After saving, the dashboard will display your signing secret (prefixed whsec_). Store it securely — you'll need it to verify webhook signatures. The secret is only shown once upon creation.
Webhook endpoints are scoped to live or sandbox mode. Sandbox endpoints only receive events from scans created with sandbox API keys, and vice versa.
Event types
Subscribe to any combination of these events when registering a webhook:
- Name
scan.pending- Description
Fired when the scan is created and queued for processing.
- Name
scan.processing- Description
Fired when the solver starts working on the scan.
- Name
scan.completed- Description
Fired once when the scan finishes successfully. The
statusfield will becompleted.
- Name
scan.failed- Description
Fired once when the scan encounters an error. The
statusfield will beerror.
Payload format
When an event fires, we send an HTTP POST request to your registered URL with a JSON body. The event type is also included in the X-Enhub-Event header.
Body
- Name
event- Type
- string
- Description
The event type (e.g.
scan.completed).
- Name
data- Type
- object
- Description
The event payload.
Data object
The data field contains:
- Name
id- Type
- string
- Description
Scan ID.
- Name
status- Type
- string
- Description
Current status. One of:
pending,processing,completed,error.
- Name
runUuid- Type
- string
- Description
The solver run UUID.
- Name
createdAt- Type
- string
- Description
When the scan was created (ISO 8601).
- Name
errorMessage- Type
- string
- Description
Error description (only present on
scan.failedevents).
Headers
Every delivery includes these headers:
- Name
X-Enhub-Event- Type
- string
- Description
The event type (e.g.
scan.completed).
- Name
X-Enhub-Signature- Type
- string
- Description
Stripe-style signature for verification. Format:
t=<timestamp>,v1=<hmac>. See Verifying signatures.
- Name
Content-Type- Type
- string
- Description
Always
application/json.
scan.completed
{
"event": "scan.completed",
"data": {
"id": "cml3ucftb0001yqzvr4jgakw5",
"status": "completed",
"runUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"createdAt": "2026-03-01T12:00:00.000Z"
}
}
scan.failed
{
"event": "scan.failed",
"data": {
"id": "cml3ucftb0001yqzvr4jgakw5",
"status": "error",
"errorMessage": "REopt solver status: timed-out",
"runUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"createdAt": "2026-03-01T12:00:00.000Z"
}
}
scan.pending
{
"event": "scan.pending",
"data": {
"id": "cml3ucftb0001yqzvr4jgakw5",
"status": "pending",
"runUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"createdAt": "2026-03-01T12:00:00.000Z"
}
}
scan.processing
{
"event": "scan.processing",
"data": {
"id": "cml3ucftb0001yqzvr4jgakw5",
"status": "processing",
"createdAt": "2026-03-01T12:00:00.000Z"
}
}
Verifying signatures
Every webhook delivery includes an X-Enhub-Signature header with a Stripe-style timestamp + HMAC-SHA256 signature. Always verify this signature to ensure the request is from Enhub and hasn't been tampered with.
The header format is:
X-Enhub-Signature: t=1709294732,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9
Verification steps
- Extract the
t(timestamp) andv1(signature) values from the header - Concatenate
{timestamp}.{rawBody}to form the signed content - Compute HMAC-SHA256 of the signed content using your signing secret's SHA-256 hash as the key
- Compare your computed signature with the
v1value using a timing-safe comparison - Optionally check timestamp freshness to prevent replay attacks
Signature verification
import crypto from 'crypto';
function verifyWebhook(rawBody, signatureHeader, secret) {
// Parse "t=<timestamp>,v1=<signature>"
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=', 2)),
);
const timestamp = parts.t;
const signature = parts.v1;
// The signing key is the SHA-256 hash of your whsec_ secret
const key = crypto.createHash('sha256').update(secret).digest('hex');
// The signed content is "timestamp.body"
const signedContent = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', key)
.update(signedContent)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// Express.js example
app.post('/webhooks/enhub', express.raw({ type: 'application/json' }), (req, res) => {
const signatureHeader = req.headers['x-enhub-signature'];
const secret = process.env.ENHUB_WEBHOOK_SECRET; // whsec_...
if (!verifyWebhook(req.body.toString(), signatureHeader, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log(`Received ${event.event} for scan ${event.data.id}`);
res.status(200).send('OK');
});
Delivery behaviour
Webhook deliveries are fire-and-forget. Each event is delivered once — there are no automatic retries.
Your endpoint must respond within 10 seconds. If the response takes longer, the request is aborted and the delivery is recorded as failed.
Every delivery attempt is logged with the request/response details. You can inspect delivery history (status codes, response bodies, duration, errors) in the dashboard by expanding a webhook endpoint.
Delivery logs are retained for 30 days and then cleaned up automatically.
Test fire
You can send a test event to any endpoint from the dashboard by clicking the play button on the endpoint row. This sends a special test event:
Test event payload
{
"event": "test",
"data": {
"message": "This is a test webhook from Enhub",
"timestamp": "2026-03-01T12:00:00.000Z"
}
}
The delivery result (success, status code, duration) appears immediately in the delivery logs.
Local development with ngrok
During development you can use ngrok to expose a local server and receive webhook events without deploying.
1. Start your local server
Run your webhook handler on a local port (e.g. 3000):
node server.js
# Listening on http://localhost:3000
2. Start ngrok
In a separate terminal, start an ngrok tunnel:
ngrok http 3000
ngrok will display a public URL like https://a1b2c3d4.ngrok-free.app.
3. Register the ngrok URL
In the Enhub dashboard under Organization → API, create a webhook endpoint with the ngrok URL as the target:
https://a1b2c3d4.ngrok-free.app/webhooks/enhub
Select the events you want to receive and save.
4. Test
Use the test fire button in the dashboard, or create a scan with a test API key. Events will be forwarded to your local server through the ngrok tunnel.
ngrok URLs change every time you restart the free tier. Update your webhook endpoint URL in the dashboard after restarting ngrok.
Terminal 1 — local server
$ node server.js
Listening on http://localhost:3000
Received scan.completed for scan cml3ucftb0001yqzvr4jgakw5
Terminal 2 — ngrok
$ ngrok http 3000
Session Status online
Forwarding https://a1b2c3d4.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
1 0 0.00 0.00 0.28 0.28
HTTP Requests
POST /webhooks/enhub 200 OK
Managing webhooks
Webhooks are managed through the Enhub dashboard under Organization → API. From there you can:
- Create an endpoint with URL, events, and optional custom headers
- View all registered webhooks and their delivery history
- Toggle an endpoint active/inactive to pause or resume deliveries
- Test by sending a test event via the play button
- Regenerate the signing secret if it has been compromised (the new secret is shown once and copied to your clipboard)
- Delete a webhook endpoint and its delivery logs permanently