Webhooks

Webhooks let your server receive scan status updates automatically instead of polling GET /v1/scans/:id/status. When a scan progresses, completes, or fails, 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 — not via the API. Navigate to Settings → Webhooks in your project to register, update, or delete webhook endpoints.

When creating a webhook you'll configure:

  • Name
    URL
    Type
    string
    Description

    The HTTPS 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.

After saving, the dashboard will display your signing secret. 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.status_update
    Description

    Fired when scan progression changes during processing. You may receive multiple events as the scan progresses from 0% to 100%.

  • Name
    scan.completed
    Description

    Fired once when the scan finishes successfully. The status field will be completed and progression will be 100.

  • Name
    scan.failed
    Description

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


Payload format

When an event fires, we send an HTTP POST request to your registered URL with a JSON body.

Envelope

  • Name
    event
    Type
    string
    Description

    The event type (e.g. scan.completed).

  • Name
    webhookId
    Type
    string
    Description

    The ID of the webhook registration that triggered this delivery.

  • Name
    timestamp
    Type
    string
    Description

    When the event was created (ISO 8601).

  • Name
    data
    Type
    object
    Description

    The event payload — same shape as the response from GET /v1/scans/:id/status.

Data object

The data field contains a scan status object with these properties:

  • Name
    id
    Type
    string
    Description

    Scan ID.

  • Name
    status
    Type
    string
    Description

    Current status. One of: processing, completed, error.

  • Name
    progression
    Type
    number
    Description

    Progression percentage (0–100).

  • Name
    hasError
    Type
    boolean
    Description

    Whether the scan has an error.

  • Name
    errorMessage
    Type
    string
    Description

    Error message (only present when hasError is true).

  • Name
    createdAt
    Type
    string
    Description

    When the scan was created (ISO 8601).

scan.completed

{
  "event": "scan.completed",
  "webhookId": "wh_cml3ucftb0001yqzvr4jgakw5",
  "timestamp": "2026-03-01T12:05:32.000Z",
  "data": {
    "id": "cml3ucftb0001yqzvr4jgakw5",
    "status": "completed",
    "progression": 100,
    "hasError": false,
    "createdAt": "2026-03-01T12:00:00.000Z"
  }
}

scan.failed

{
  "event": "scan.failed",
  "webhookId": "wh_cml3ucftb0001yqzvr4jgakw5",
  "timestamp": "2026-03-01T12:05:32.000Z",
  "data": {
    "id": "cml3ucftb0001yqzvr4jgakw5",
    "status": "error",
    "progression": 45,
    "hasError": true,
    "errorMessage": "Scan timed out na 12 minuten. De berekening duurde te lang — probeer het opnieuw.",
    "createdAt": "2026-03-01T12:00:00.000Z"
  }
}

scan.status_update

{
  "event": "scan.status_update",
  "webhookId": "wh_cml3ucftb0001yqzvr4jgakw5",
  "timestamp": "2026-03-01T12:02:15.000Z",
  "data": {
    "id": "cml3ucftb0001yqzvr4jgakw5",
    "status": "processing",
    "progression": 45,
    "hasError": false,
    "createdAt": "2026-03-01T12:00:00.000Z"
  }
}

Verifying signatures

Every webhook delivery includes an X-Enhub-Signature header containing an HMAC-SHA256 signature of the raw request body, using your webhook secret as the key. Always verify this signature to ensure the request is from Enhub.

Verification steps

  1. Read the raw request body (before JSON parsing)
  2. Compute HMAC-SHA256 of the body using your webhook secret
  3. Compare the result with the X-Enhub-Signature header value
  4. Reject the request if the signatures don't match

Signature verification

import crypto from 'crypto';

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .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 signature = req.headers['x-enhub-signature'];
  const secret = process.env.ENHUB_WEBHOOK_SECRET;

  if (!verifyWebhook(req.body, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log(`Received ${event.event} for scan ${event.data.id}`);

  // Process the event...
  res.status(200).send('OK');
});

Retry policy

If your endpoint doesn't return a 2xx status code, we retry the delivery with exponential backoff:

  • Name
    Attempt 1
    Description

    Immediate delivery.

  • Name
    Attempt 2
    Description

    30 seconds after the first attempt.

  • Name
    Attempt 3
    Description

    5 minutes after the second attempt.

  • Name
    Attempt 4
    Description

    30 minutes after the third attempt.

After 4 failed attempts, the delivery is marked as failed. If a webhook endpoint consistently fails, it will be automatically deactivated after 10 consecutive failures.


Managing webhooks

Webhooks are managed entirely through the Enhub dashboard under Settings → Webhooks. From there you can:

  • View all registered webhooks and their delivery history
  • Edit the URL, subscribed events, or active status
  • Pause deliveries by toggling a webhook to inactive
  • Delete a webhook registration permanently
  • Rotate the signing secret if it has been compromised

Was this page helpful?