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:

  1. Create a scan via POST /v1/scans
  2. Poll GET /v1/scans/:id/status every few seconds
  3. Stop polling when status is completed or error

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 2xx status 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.


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 status field will be completed.

  • Name
    scan.failed
    Description

    Fired once when the scan encounters an error. The status field will be error.


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.failed events).

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

  1. Extract the t (timestamp) and v1 (signature) values from the header
  2. Concatenate {timestamp}.{rawBody} to form the signed content
  3. Compute HMAC-SHA256 of the signed content using your signing secret's SHA-256 hash as the key
  4. Compare your computed signature with the v1 value using a timing-safe comparison
  5. 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.

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.

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

Was this page helpful?