Gareth Wilson Gareth Wilson

Guide to HiBob Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationThe Bob UI, with a URL (up to 2048 characters) and a choice of events
Connection testA POST with "Ping test" when the webhook is added. Must get 200
Signature headerBob-Signature
SchemeHMAC-SHA512, base64 digest
Signed contentThe raw request body, nothing else
SecretPer webhook, shown in the Bob UI
Replay windowNone. No timestamp is signed
IP allowlistingSupported. HiBob publishes its IPs in a help centre article
Response window200 within 10 seconds
RetriesExponential backoff for up to 3 days
DeactivationAfter 72 hours with no successful delivery on 3 consecutive days
Event retentionAt least two working days
Orderingv2 says events are sent in the right order

Common events

HiBob documents 29 v2 event types across five modules:

ModuleEvent typesFires when
Employee dataemployee.created, employee.updated, employee.deletedAn employee record is created, changed or deleted
Employee lifecycleemployee.joined, employee.left, employee.activated, employee.inactivated, employee.temporaryLeave, employee.access.revoked.immediatelyA lifecycle entry becomes effective: access granted or enabled, termination, leave or temporary leave, or a termination with access revoked immediately
Custom tablestable.entry.created, table.entry.updatedA table entry is added or changed
Time offtimeoff.request.requested, timeoff.request.approved, timeoff.request.declined, timeoff.request.cancelled, timeoff.request.deleted, timeoff.request.updated, timeoff.request.imported, timeoff.request.setEndDateA time off request moves through its lifecycle
Taskstodo.assign, todo.changedStatusA task is assigned or changes status
Docsdocuments.eSign.completedEvery signer has finished signing a document
Workforce planningworkforcePlanning.position.created, workforcePlanning.position.updated, workforcePlanning.positionOpening.created, workforcePlanning.positionOpening.updated, workforcePlanning.positionOpening.deleted, workforcePlanning.positionBudget.created, workforcePlanning.positionBudget.updatedA 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.

  1. 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.
  2. Bob sends a POST with the text "Ping test", or the JSON {"text":"Ping test"}, and expects a 200. Your endpoint must answer it straight away.
  3. Copy the webhook's secret from the Bob UI and store it server-side as HIBOB_WEBHOOK_SECRET.
  4. 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-Signature with 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 200 before 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 fieldUpdates to 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 in data and triggeredAt, 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.