
BambooHR webhooks vs. polling: what each one covers

Summarise the blog with AI
Key takeaways
- BambooHR webhooks watch only employee and custom fields; custom-table fields, benefits, dependents, and time off can't be monitored.
- A permissioned webhook is created through the API and inherits whatever fields its creator can access, unlike an admin-configured global webhook.
- Every delivery is signed with HMAC-SHA256 over the raw body and the timestamp header, and the private key is shown only once, at creation.
- Failed deliveries retry up to five times, each delay padded with random jitter, but a 4xx response is never retried.
- The changed-employees endpoint documents no pagination or result cap as of September 2026, and its table variant can return every row an employee has, not just the changed ones.
- Benefits, dependents, and time off sit outside both signals. Time off has its own
updatedSincefilter, benefits have a one-year event history, and dependents need a scheduled read-and-diff.
BambooHR webhooks tell you the moment an employee's job title changes. They stay silent when that same employee enrolls in a health plan. For a benefits platform, that's the data the integration exists to move.
BambooHR's own documentation draws the boundary in two places, and they don't move independently. Webhooks are scoped to employee fields and custom fields, never a custom table, never benefits, dependents, or time off. Poll /employees/changed instead, and the same boundary follows you there, an idea this page comes back to once you've seen why.
This page maps every BambooHR data type to the mechanism that reports on it: what webhooks watch, how to receive them without losing data, what happens when delivery fails, how to poll as a recovery net, and what to run for the data neither signal covers. If you haven't set up API access yet, that groundwork lives in our BambooHR API guide; this one assumes you're past that.
What a BambooHR webhook can see
BambooHR splits its push channel into two configurations, and which one you're running decides what you see. A global webhook is set up by an admin inside the BambooHR UI and watches a predefined list of standard fields. A permissioned webhook is created through the API instead, and it watches whatever field the creating API user has permission to read, standard employee field or custom field alike.
BambooHR's Webhooks documentation confirms this boundary: the monitorable set is limited to employee and custom fields, and custom-table fields aren't included in either webhook type.
Three employee events exist. employee.updated needs at least one monitored field configured, fires only when a monitored field changes, and carries a changedFields array naming what moved. employee.created and employee.deleted carry no such array, so a handler that needs a new hire's starting values still has to fetch the record. BambooHR recommends event-based webhooks over its older field-based format. One catch: a webhook created through the API without naming any events defaults to the legacy field-based behavior, so list employee.created, employee.updated, and employee.deleted explicitly.
A permissioned webhook built on a lower-privileged service account will quietly under-report: employee.updated doesn't fire for changes to fields its creator can't read, while employee.created and employee.deleted skip that check entirely, and nothing in the delivery tells you what went missing. It also stops working if the creating user's BambooHR account is deactivated, so tie it to a service account nobody offboards.
Receiving BambooHR webhook deliveries safely
Creating a webhook is the easy part. Everything that makes the channel trustworthy happens in how you receive it.
- Create the webhook: send the
POSTto/api/v1/webhookswith a target URL that starts withhttps://. - Store the private key immediately: it comes back once, in the creation response, and BambooHR will not show it again.
- Verify every delivery: recompute the HMAC-SHA256 signature from the raw request body and the
X-BambooHR-Timestampheader using the stored key, then compare it toX-BambooHR-Signature. - Parse by format: an event-based delivery carries one employee, with
type,timestamp, and adataobject holdingemployeeIdand, on updates,changedFields. A legacy field-based delivery can batch several employees underemployees, each with its ownidandchangedFields. Either way, ignore fields you don't recognize, since the payload can change shape over time. - Enqueue before you acknowledge: write the batch to a queue and return success, so a slow downstream call can't cost you the delivery.
For the general mechanics of HMAC signing, we cover those separately in our guide to how HMAC secures webhooks; what follows is only the BambooHR-specific shape.
BambooHR's webhook documentation confirms the header names: X-BambooHR-Timestamp and X-BambooHR-Signature on every delivery.
Two timing quirks are easy to miss. Creating an employee fires employee.created and then employee.updated right after, so a handler that only listens for created misses whatever the second event carries. Several fields changed at once may be consolidated into one updated event, though BambooHR groups custom-field and standard-field changes separately, so one save can produce two events. A history-tracked field, like job title or pay rate, only fires when the change takes effect: enter a raise dated for next month, and the webhook fires next month, however early the record was updated.
A correctly verified handler still assumes your endpoint is up to receive it. The next question is what BambooHR does when it isn't.
What happens when a delivery fails
Per BambooHR's webhook documentation, a failed delivery is retried up to five times, on this schedule:
Each delay is padded with 0 to 30 seconds of random jitter, and BambooHR's docs don't say whether it's measured from the previous attempt or from the original send, so treat the numbers as spacing, not a precise clock. Retries run only for network errors or a 5xx response from your endpoint. A 4xx is never retried: BambooHR treats it as a deliberate rejection, so a 401 from a misconfigured signature check is exactly as final as a 200.
That schedule is also why a handler needs to be idempotent. A delivery that succeeded on your end but timed out before BambooHR saw the response will arrive again. Dedupe on the event type, the employee ID, and the event's own timestamp. Keying on the employee ID and changedFields alone would also swallow a second, legitimate change to the same field.
If you need to confirm what was actually sent, BambooHR's delivery log endpoint covers only the last 14 days, caps at 200 entries, and returns an empty array if nothing went out in that window. Past that, whatever you persisted on receipt is the only record left.
Five retries and a 14-day log is a recovery window, not a guarantee. Anything that falls outside it needs a pull mechanism that doesn't depend on BambooHR ever delivering it.
Polling BambooHR for changes you might have missed
Run GET /api/v1/employees/changed with a URL-encoded ISO 8601 since, and BambooHR returns every employee changed after that point. An optional type filter narrows the result to inserted, updated, or deleted.
Each entry carries its own last-changed timestamp, and the response as a whole carries the latest one across the result set. Store that value and pass it back as your next since. A timestamp you generate locally is the wrong checkpoint, because BambooHR's clock decides what counts as changed.
The endpoint documents no cursor, page size, or maximum result count, as of September 2026, so build the poll assuming a single response could be large.
The table endpoint costs more than the trigger suggests. Because it's keyed on the employee's last-changed timestamp, one unrelated change to that employee returns every row they have in the named table, so syncing job history for a large company means re-reading rows that never changed.
Throttling carries no published numeric limit. Per BambooHR's Planned Changes page, since September 16, 2026, a throttled request returns 429 with a Retry-After header, replacing the 503 BambooHR used before; honor that header. BambooHR's own Technical Overview still describes 429 as an employee-count limit, so the two documents disagree on what triggers it, another reason to trust the header over the status code alone.
Between the two signals, you have pushes for employee and custom fields, and a poll that catches whatever those miss, plus the job, compensation, and status history tables. That covers the employee record. It doesn't cover what a benefits platform needs most.
What neither signal covers: benefits, dependents, and time off
Both signals answer to the same trigger. The changed-employees endpoint fires on any change to an individual employee field, or to the employment status, job info, or compensation tables, and a webhook fires on whatever subset of those fields is monitored. Benefits, dependents, and time off never make either list.
That's the documented boundary of what BambooHR's change signals watch, so your wiring isn't the problem. A dependent's date of birth changing, or a mid-year plan election, updates a part of BambooHR's data model that neither the webhook system nor the changed endpoint is documented to track.
Each of the three needs its own route:
- Time off has the closest thing to a change feed. List Time Off Requests takes an
updatedSincetimestamp and returns requests created, approved, denied, canceled, or edited since then. BambooHR's reference says deleted requests aren't reported and recommends polling with an overlap, so pair it with an occasional full window read. - Benefits have an event history.
GET /api/v1/benefit/member_benefitreturns the past year of coverage events per member (eligibility granted, enrolled, loss of coverage), for employees and dependents alike. It takes nosinceparameter, so you diff it against what you stored. Contributions and coverage level come fromGET /api/v1/benefit/employee_benefit, and the paginatedGET /api/v1/benefits/member-benefitsgives a calendar-year snapshot per member. - Dependents come from
GET /api/v1/employeedependents, which returns every dependent (or one employee's) with no change filter.
Wherever there's no filter, the pattern is a scheduled read-and-diff: pull the current state on an interval, compare it to what you stored last time, and treat the difference as the change event BambooHR won't hand you.
Putting webhooks, polling, and scheduled reads together
Turning a change signal into an updated record is a separate read, covered in our guide to fetching employee data from BambooHR. The bigger question is how all three mechanisms fit together as one system.
Laid out by data type, the map looks like this:
Running all of this yourself means three pipelines: a webhook receiver with signature verification and idempotent handling, a checkpointed poll for whatever the webhook missed, and scheduled reads for everything neither one watches. Each has its own failure mode, and none substitutes for the others.
A unified API can run that third pipeline for you. Bindbee's BambooHR connector syncs on a 24-hour default schedule, adjustable per connection, and sends its own webhooks when a sync finds created or updated records or when a sync starts, finishes, or fails. That is not a live feed, and a direct BambooHR webhook is still lower latency than a 24-hour sync for the fields it covers.
Where it earns its place is the side BambooHR leaves to scheduled reads. For BambooHR, Bindbee's docs list read support for Dependent, Time Off, and four benefits models: Employer Benefit (the plan and its deduction code), Benefit (one employee's enrollment, with coverage tier and contributions), Dependent Benefit, and Benefit Coverage (insured amounts per covered person). If that gap is the one costing your team engineering time, see what Bindbee's BambooHR connector syncs.
The choice was never webhooks or polling. It's a map: one mechanism per data type, a recovery path for the ones that can fail silently, and a scheduled read for the ones with no signal at all.
Frequently asked questions
Does BambooHR support webhooks?
Yes, in two forms. Global webhooks are configured by an admin in the BambooHR UI and watch a predefined list of standard fields. Permissioned webhooks are created through the API and watch whatever fields the creating user can read. Both fire employee.created, employee.updated, and employee.deleted events.
Can BambooHR webhooks track benefits, dependents, or time off?
No. Webhooks are limited to employee fields and custom fields. Time off has its own updatedSince filter, benefits have a one-year event history on member_benefit, and dependents need a scheduled read-and-diff.
How do I verify a BambooHR webhook delivery?
Recompute the HMAC-SHA256 signature over the raw request body and the X-BambooHR-Timestamp header using the private key from the creation response, then compare it to X-BambooHR-Signature. The key is shown only once, so store it immediately.
What happens if my webhook endpoint is down?
BambooHR retries a failed delivery up to five times, for network errors or 5xx responses, with random jitter on each delay. A 4xx is never retried. The delivery log covers only the last 14 days and caps at 200 entries.
How do I catch BambooHR changes a webhook missed?
Poll GET /api/v1/employees/changed with a since timestamp, then store the latest timestamp from the response and use it as the next since, rather than a timestamp from your own clock.



.jpg)


