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
- Create a webhook endpoint -- register a URL and select which events you want to receive
- Receive events -- OnboardingHub sends a
POSTrequest to your URL with event data - Verify the signature -- validate the HMAC signature to ensure the payload is authentic
- Return 2xx -- respond with a 2xx status code to acknowledge receipt
Creating a webhook endpoint
- Go to Integrations > Webhooks in your workspace settings
- Click Add Endpoint
- 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_idto deduplicate events in case of retries - Monitor delivery health -- check the endpoint's
failure_countand recent deliveries - Use the test endpoint -- verify connectivity before subscribing to production events
Next steps
- Zapier integration -- connect OnboardingHub to Zapier
- n8n integration -- connect OnboardingHub to n8n