Webhooks

Webhooks let you receive real-time HTTP notifications when events happen in your OnboardingHub workspace. Instead of polling the API, OnboardingHub pushes event data to your server as it occurs.

How it works

  1. Create a webhook endpoint -- register a URL and select which events you want to receive
  2. Receive events -- OnboardingHub sends a POST request to your URL with event data
  3. Verify the signature -- validate the HMAC signature to ensure the payload is authentic
  4. Return 2xx -- respond with a 2xx status code to acknowledge receipt

Creating a webhook endpoint

  1. Go to Integrations > Webhooks in your workspace settings
  2. Click Add Endpoint
  3. Enter the URL, select the events you want to receive, and save

Important: The signing secret is shown only once when the endpoint is created. Copy and store it securely.

Event types

Event Trigger
enrollment.created A contact is enrolled in a guide
enrollment.started A contact begins a guide (status changes to in_progress)
enrollment.completed A contact finishes all steps (status changes to completed)
enrollment.expired An enrollment expires
guide.viewed A contact views a guide
step.completed A contact completes a step
section.completed A contact completes all steps in a section
file.uploaded A contact uploads a file in a file upload step
ces.submitted A contact submits a Customer Effort Score

Payload format

Every webhook delivery sends a JSON POST request with this structure:

{
  "event": "enrollment.created",
  "timestamp": "2026-02-09T12:00:00Z",
  "webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "data": {
    "id": "01234567-89ab-cdef-0123-456789abcdef",
    "status": "invited",
    "access_token": "abc123",
    "progress_percent": 0,
    "started_at": null,
    "completed_at": null,
    "created_at": "2026-02-09T12:00:00Z",
    "updated_at": "2026-02-09T12:00:00Z",
    "guide": {
      "id": "fedcba98-7654-3210-fedc-ba9876543210",
      "name": "Getting Started Guide",
      "status": "published",
      "created_at": "2026-01-10T09:00:00Z"
    },
    "contact": {
      "id": "11111111-2222-3333-4444-555555555555",
      "email": "[email protected]",
      "first_name": "Jane",
      "last_name": "Doe",
      "created_at": "2026-01-05T14:00:00Z"
    }
  }
}

HTTP headers

Each webhook delivery includes these headers:

Header Description
Content-Type application/json
X-OnboardingHub-Signature HMAC-SHA256 signature of the request body
X-OnboardingHub-Event The event type (e.g., enrollment.created)
X-OnboardingHub-Delivery Unique delivery ID (UUID)

Verifying signatures

Every webhook payload is signed with HMAC-SHA256 using your endpoint's signing secret. You should verify the signature to ensure the request is authentic.

The signature is in the X-OnboardingHub-Signature header with the format sha256=<hex_digest>.

Ruby

def verify_webhook(request, secret)
  payload = request.body.read
  signature = request.headers["X-OnboardingHub-Signature"]
  expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, payload)

  unless ActiveSupport::SecurityUtils.secure_compare(signature, expected)
    raise "Invalid webhook signature"
  end

  JSON.parse(payload)
end

Python

import hmac
import hashlib
import json

def verify_webhook(request, secret):
    payload = request.body
    signature = request.headers.get("X-OnboardingHub-Signature", "")
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid webhook signature")

    return json.loads(payload)

JavaScript (Node.js)

const crypto = require("crypto");

function verifyWebhook(req, secret) {
  const payload = req.body; // raw body string
  const signature = req.headers["x-onboardinghub-signature"];
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(payload).digest("hex");

  if (
    !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    throw new Error("Invalid webhook signature");
  }

  return JSON.parse(payload);
}

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
)

func verifyWebhook(r *http.Request, secret string) ([]byte, error) {
    body, _ := io.ReadAll(r.Body)
    signature := r.Header.Get("X-OnboardingHub-Signature")

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(signature), []byte(expected)) {
        return nil, fmt.Errorf("invalid webhook signature")
    }

    return body, nil
}

Retry policy

If your endpoint does not respond with a 2xx status code, OnboardingHub retries the delivery with exponential backoff:

Attempt Delay
1st retry 5 seconds
2nd retry 30 seconds
3rd retry 2 minutes
4th retry 15 minutes
5th retry 1 hour
6th retry 4 hours

After 6 failed attempts, the delivery is marked as permanently failed.

Connection timeouts

  • Connect timeout: 10 seconds
  • Read timeout: 30 seconds

Auto-disable

Endpoints that fail consistently are automatically disabled:

  • After 15 consecutive failures spanning at least 3 days, the endpoint is disabled
  • Disabled endpoints stop receiving deliveries
  • You can re-enable a disabled endpoint from Integrations > Webhooks

URL requirements

  • Webhook URLs must use HTTPS
  • URLs resolving to private IP ranges (10.x, 172.16.x, 192.168.x, 127.x, link-local) are rejected
  • This is an SSRF protection measure

Testing webhooks

Click the Test button on any webhook endpoint in Integrations > Webhooks to send a test ping event. The result shows whether delivery succeeded along with the HTTP response code from your server.

Best practices

  • Always verify signatures -- never trust unverified payloads
  • Respond quickly -- return 2xx within 30 seconds; process the event asynchronously
  • Be idempotent -- use the webhook_id to deduplicate events in case of retries
  • Monitor delivery health -- check the endpoint's failure_count and recent deliveries
  • Use the test endpoint -- verify connectivity before subscribing to production events

Next steps