Guide to HiBob Webhooks: Features and Best Practices
HiBob webhooks tell your systems when something changes in Bob, HiBob's HR platform: an employee is hired, updated, starts or leaves, a time off request is submitted or approved, a task is assigned, a document is signed, or a position is opened in workforce planning. If you sync people data into identity providers, payroll, IT provisioning or your own data warehouse, webhooks let those systems react to changes without polling Bob's API.
This guide covers HiBob's Webhooks v2: how they are configured, the Ping test, how to verify the Bob-Signature header, the events and payload, the retry and deactivation rules, and the best practices for running a HiBob receiver in production.
What are HiBob webhooks?
Bob webhooks "let you subscribe to system events and receive programmatic notifications of changes as they occur". You add a webhook in the Bob UI with your URL and the events you want, and Bob sends a JSON POST to that URL whenever one of those events happens.
There are two generations. Webhooks v2 became available on January 21, 2025, and HiBob recommends it for all new work because it "will eventually replace Webhooks v1". Compared with v1, v2 sends less data, says it delivers events "in the right order", and handles retries better. This guide describes v2. A v2 body carries "version": "v2"; a v1 body has no version field. HiBob Marketplace partner apps have a different webhook setup that is not covered here.
The v2 payload carries no record data. It names the event and the ID of the thing that changed, and for updates the IDs of the fields that changed. You then read the current data from Bob's API.
HiBob webhook features
| Feature | Details |
|---|---|
| Configuration | The Bob UI, with a URL (up to 2048 characters) and a choice of events |
| Connection test | A POST with "Ping test" when the webhook is added. Must get 200 |
| Signature header | Bob-Signature |
| Scheme | HMAC-SHA512, base64 digest |
| Signed content | The raw request body, nothing else |
| Secret | Per webhook, shown in the Bob UI |
| Replay window | None. No timestamp is signed |
| IP allowlisting | Supported. HiBob publishes its IPs in a help centre article |
| Response window | 200 within 10 seconds |
| Retries | Exponential backoff for up to 3 days |
| Deactivation | After 72 hours with no successful delivery on 3 consecutive days |
| Event retention | At least two working days |
| Ordering | v2 says events are sent in the right order |
Common events
HiBob documents 29 v2 event types across five modules:
| Module | Event types | Fires when |
|---|---|---|
| Employee data | employee.created, employee.updated, employee.deleted | An employee record is created, changed or deleted |
| Employee lifecycle | employee.joined, employee.left, employee.activated, employee.inactivated, employee.temporaryLeave, employee.access.revoked.immediately | A lifecycle entry becomes effective: access granted or enabled, termination, leave or temporary leave, or a termination with access revoked immediately |
| Custom tables | table.entry.created, table.entry.updated | A table entry is added or changed |
| Time off | timeoff.request.requested, timeoff.request.approved, timeoff.request.declined, timeoff.request.cancelled, timeoff.request.deleted, timeoff.request.updated, timeoff.request.imported, timeoff.request.setEndDate | A time off request moves through its lifecycle |
| Tasks | todo.assign, todo.changedStatus | A task is assigned or changes status |
| Docs | documents.eSign.completed | Every signer has finished signing a document |
| Workforce planning | workforcePlanning.position.created, workforcePlanning.position.updated, workforcePlanning.positionOpening.created, workforcePlanning.positionOpening.updated, workforcePlanning.positionOpening.deleted, workforcePlanning.positionBudget.created, workforcePlanning.positionBudget.updated | A position, opening or budget is created, changed or deleted |
Event types are case-sensitive and use camelCase segments, such as timeoff.request.setEndDate and workforcePlanning.positionOpening.deleted. Match them exactly.
Every v2 body has the same envelope. Here is HiBob's employee.updated example:
{
"companyId": 637323,
"type": "employee.updated",
"triggeredBy": "3332883804594373380",
"triggeredAt": "2024-12-26T11:29:14.399997",
"version": "v2",
"data": {
"employeeId": "3332883894235038486",
"fieldUpdates": [
{ "id": "personal.birthDate" }
]
}
}
type is the event, triggeredBy is the backend ID of the employee who made the change or system for automatic events, and triggeredAt is when it happened. data holds identifiers only: employeeId here, or requestId, positionId and similar for other modules, plus the changed field IDs on updates. For custom table events, HiBob also sends a Get-Table header with the API URL to read the entry.
Setting up HiBob webhooks
Start your endpoint first, because Bob tests it when you add the webhook.
- In Bob, add a webhook with your HTTPS URL and choose the events you want. HiBob's help centre article on managing webhooks has the click path for your account.
- Bob sends a
POSTwith the text "Ping test", or the JSON{"text":"Ping test"}, and expects a200. Your endpoint must answer it straight away. - Copy the webhook's secret from the Bob UI and store it server-side as
HIBOB_WEBHOOK_SECRET. - Optionally, allowlist Bob's IP addresses as an extra check. HiBob lists them in its help centre.
For local development, the Hookdeck CLI (hookdeck listen 3000 hibob --path /webhooks/hibob) gives you a public HTTPS URL that forwards to your local server, with no account required. Use it as the webhook URL, then change an employee record to trigger employee.updated.
Securing HiBob webhooks
Read Bob-Signature, compute an HMAC-SHA512 of the raw request body with your webhook secret as the key, encode the 64-byte digest as standard base64 (88 characters ending in ==), and compare in constant time. HiBob's Scala, PHP, PowerShell and Node samples all produce single-line base64. The Ruby sample uses Base64.encode64, which inserts line breaks, so use Base64.strict_encode64 if you write Ruby.
HiBob does not say whether the Ping test is signed. Answer any body that is exactly a Ping test with a 200 before running verification, and verify everything else.
const crypto = require('crypto');
const express = require('express');
function verifyBobSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const expected = crypto.createHmac('sha512', secret).update(rawBody).digest('base64');
const received = Buffer.from(signatureHeader.trim(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
function isPingTest(rawBody) {
const text = rawBody.toString('utf8').trim();
if (text === 'Ping test') return true;
try {
const parsed = JSON.parse(text);
return parsed !== null && typeof parsed === 'object' && Object.keys(parsed).length === 1 && parsed.text === 'Ping test';
} catch {
return false;
}
}
const app = express();
app.post('/webhooks/hibob', express.raw({ type: '*/*' }), (req, res) => {
// The Ping test carries no event, so answering it reveals nothing.
if (isPingTest(req.body)) return res.sendStatus(200);
if (!verifyBobSignature(req.body, req.get('bob-signature'), process.env.HIBOB_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Queue a fetch of event.data.employeeId (or requestId, positionId), then answer.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
import os
def verify_bob_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha512).digest()
expected = base64.b64encode(digest)
return hmac.compare_digest(signature_header.strip().encode("utf-8"), expected)
# In FastAPI: raw_body = await request.body()
# verify_bob_signature(raw_body, request.headers.get("bob-signature"),
# os.environ["HIBOB_WEBHOOK_SECRET"])
To check your verifier, use the secret hibob-test-secret and this exact body:
{"companyId":637323,"type":"employee.created","triggeredBy":"system","triggeredAt":"2024-12-26T11:29:14.399997","version":"v2","data":{"employeeId":"3332883894235038486"}}
The signature should be:
njxzYmiyP+YcmqjZAB+34FA1yC2XvHu0Y9sraL5GIaTSBeQnZLSOTtOIeWrq5naDBpLVsVX27fCAnbmvmNSGng==
Make HiBob webhooks production-ready. Hookdeck Event Gateway verifies
Bob-Signaturewith HMAC-SHA512, answers Bob within its 10-second window, and durably queues every employee and time off event before your service sees it.
HiBob webhook limitations and pain points
The Ping test fails the setup
The Problem: You add the webhook in Bob and it reports a failed connection.
Why It Happens: Bob's connection test is a POST whose body is plain text or a one-field JSON object, not an event. A handler that rejects unsigned requests, insists on a type field, or isn't running yet fails it.
Workarounds:
- Recognise the Ping test and answer
200before verification, as above. - Start your endpoint and any tunnel before adding the webhook.
How Hookdeck Can Help: A Hookdeck source answers Bob's requests with a 200 straight away, so the test passes even while your service is offline, and the request appears in the log for you to inspect.
The payload doesn't contain the data
The Problem: employee.updated arrives, but it only lists personal.birthDate as changed, not the new value.
Why It Happens: v2 deliberately sends only IDs and changed field IDs. HiBob says "to pull the data itself, please use our API".
Workarounds:
- Treat each webhook as a prompt to fetch the employee, request or position from Bob's API.
- Use
fieldUpdatesto decide whether the change matters to you before making the API call. - Run the fetch from a queue worker after the webhook has been answered.
How Hookdeck Can Help: Hookdeck delivers events to your worker at a max delivery rate you set, which keeps the follow-up API calls steady during a bulk import or reorganisation.
Three days of failures deactivates the webhook
The Problem: Events stop, and the webhook shows as Inactive in Bob.
Why It Happens: Bob waits 10 seconds for a 200 and retries with exponential backoff for up to 3 days. It emails Bob admins after 1, 24 and 48 hours (status "Delivery error"), and at 72 hours deactivates the webhook (status "Inactive") if there were no successful deliveries on 3 consecutive days. Bob keeps events for at least two working days, with Friday to Sunday counted as non-working days.
Workarounds:
- Answer within 10 seconds, and do the API fetch in a worker.
- Add the people who own the integration to Bob's webhook failure emails.
- If a webhook goes Inactive, fix the endpoint and reactivate it promptly, inside the retention window.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, so your downtime doesn't count against Bob's retry budget. Events are delivered to your service with retries when it is ready, and can be replayed.
No event ID
The Problem: A retried delivery is processed twice.
Why It Happens: v2 bodies have no event ID field, and retries resend the same event.
Workarounds:
- Build a key from
type, the main ID indataandtriggeredAt, and store it before processing. - Make the downstream work idempotent. Fetching the latest record and writing it again is safe to repeat.
How Hookdeck Can Help: Hookdeck deduplication can drop repeated deliveries based on the fields you choose, such as type, data.employeeId and triggeredAt.
Best practices
Verify against the raw body
Mount express.raw() on the route, read await request.body() in FastAPI, or use Buffer.from(await request.arrayBuffer()) in a Next.js route handler. Parse only after verifying.
Route on type, exactly
Match the full, case-sensitive type string, and keep a default branch that logs and acknowledges anything new.
Check the version
If you might receive v1 webhooks from older configurations, branch on the presence of version: "v2" and handle v1 bodies separately or migrate them.
Acknowledge fast and process asynchronously
Return 200 within 10 seconds and fetch the record from Bob's API afterwards. See why to process webhooks asynchronously.
Deduplicate deliveries
See our guide to webhook idempotency for patterns that work when the payload has no event ID.
Conclusion
HiBob's v2 webhooks are signed with a base64 HMAC-SHA512 of the raw body in Bob-Signature. Answer the Ping test, verify every event against the raw bytes, route on the exact type, fetch the record from Bob's API because the payload only carries IDs, and answer within 10 seconds so the three-day retry window never runs out.
Hookdeck Event Gateway verifies each signature, answers Bob immediately and holds every event until your service has processed it, so an outage on your side doesn't deactivate the webhook.
Get started with Hookdeck for free and handle HiBob webhooks reliably in minutes.