Skip to content

How to Integrate with the BambooHR API (2026 Engineering Guide)

BambooHR API guide: auth, pagination, rate limits, and hidden costs. Plus a Quick Start for pulling users from every connected SaaS app through a single unified API.

Sidharth Verma Sidharth Verma · · 21 min read
How to Integrate with the BambooHR API (2026 Engineering Guide)

Your engineer just said integrating with BambooHR will take a sprint, falling into the classic "just a few API calls" trap. They're looking at the REST endpoints, the clean JSON responses, and the straightforward Basic Auth header. They're right about the happy path — that part genuinely is simple. What they haven't priced in is the undocumented throttling, the 400-field ceiling you'll hit the moment you try to pull a full employee record, and the OAuth migration BambooHR started enforcing in 2025.

This guide breaks down the architectural realities of building a native BambooHR integration. We cover authentication mechanics, pagination constraints, rate limit handling, webhook architecture, and the hidden maintenance costs that drain engineering resources quarter after quarter.

If your real goal is broader - pulling a list of users from every SaaS app your customers have connected, not just BambooHR - skip ahead to the Quick Start. It covers how to aggregate users across BambooHR, Okta, Google Workspace, and dozens of other providers through a single API, with no per-provider code required.

Understanding the BambooHR API Architecture

The BambooHR API is a RESTful service where all requests route over HTTPS to a company-specific base URL:

https://api.bamboohr.com/api/gateway.php/{yourSubdomain}/v1/

Unlike centralized APIs where all traffic hits a single api.vendor.com host, BambooHR requires the customer's exact company subdomain to construct the base URL. This matters — your application must capture this information during the onboarding flow.

Each employee has an immutable employee ID that is unique within a single company that you can use to reference the employee. The API covers employees, time-off, benefits, time tracking, applicant tracking, webhooks, and custom reports. You can request specific fields by passing a comma-separated list in the fields query parameter:

curl -u "{API_KEY}:x" \
  "https://api.bamboohr.com/api/gateway.php/acme/v1/employees/123?fields=firstName,lastName,department,jobTitle"

Currently, the only version is "V1." However, BambooHR has added three new Datasets endpoints under /api/v1_2/ that provide the same dataset and field discovery capabilities as their /api/v1/ counterparts, with improved API consistency. For new integrations, it's worth evaluating the v1.2 endpoints alongside the standard v1 surface.

Core entities in the data model:

  • Employees: The central node. Returns demographic data, contact information, and organizational placement.
  • Employment Status: Historical and current records of an employee's lifecycle (hired, terminated, promoted).
  • Time Off: Balances, policies, and individual time-off requests.
  • Custom Tables: BambooHR allows administrators to define custom fields and tables, meaning the schema you expect will rarely match the schema your enterprise customers actually use.

BambooHR provides official SDKs to help you integrate with their API quickly and reliably. Their SDKs handle authentication, error handling, retries, and provide fully typed models. As of early 2026, official SDKs exist for PHP and Python, both supporting OAuth 2.0.

The Subdomain Routing Problem

Because the base URL requires a subdomain, your application must capture this information during onboarding. If a user inputs the wrong subdomain, or if their company rebrands and changes their BambooHR URL, your integration instantly breaks with a 404. Your database schema for storing connected accounts must treat the subdomain as a mutable configuration variable, not a static identifier.

Authentication: API Keys vs. OAuth 2.0

BambooHR supports two authentication methods, and the distinction matters more than it used to.

API Key via Basic Auth

The simplest path. BambooHR uses Basic Authentication. For the username, you use your API key and for the password, you can use anything (commonly 'x'). The API key inherits the permission set of the user who created it. If a user doesn't have access to certain fields, the API key they generate won't have access to those fields either.

// Formatting the BambooHR auth header in Node.js
const apiKey = process.env.BAMBOOHR_API_KEY;
const encodedCredentials = Buffer.from(`${apiKey}:x`).toString('base64');
 
const headers = {
  'Authorization': `Basic ${encodedCredentials}`,
  'Accept': 'application/json'
};
 
const response = await fetch(
  `https://api.bamboohr.com/api/gateway.php/${subdomain}/v1/employees/directory`,
  { method: 'GET', headers }
);

Two gotchas that bite people in production:

  1. Key lockout. If an unknown API key is used repeatedly, the API will disable access for a period of time. Users will still be able to log in to the BambooHR website during this time. When the API is disabled, it will send back an HTTP 403 Forbidden response to any requests it receives. If you're debugging with the wrong key in a tight loop, you'll lock yourself out.

  2. Double round-trips. When an API request is made without credentials, BambooHR returns a 401 response with a WWW-Authenticate: Basic realm="..." header. Some HTTP clients use this as a signal to retry the request with credentials — a pattern known as HTTP authentication negotiation. This doubles the number of HTTP round trips for every API call. BambooHR strongly recommends configuring your integration to include credentials on every API request from the start rather than relying on the challenge-response cycle.

Warning

Always send the Authorization header preemptively. Relying on the 401 challenge-response pattern wastes your rate limit budget on failed requests and doubles latency.

Storing Credentials Securely

API keys grant broad access to sensitive HR data — compensation, social security numbers, home addresses. Storing these in plain text in your database is a massive liability. Your infrastructure must encrypt keys at rest using AES-256-GCM or equivalent, decrypting them only in memory immediately before the HTTP request is dispatched.

OAuth 2.0 (Authorization Code Flow)

For multi-tenant integrations or marketplace apps, OAuth 2.0 is now the required path. BambooHR deprecated its OpenID Connect Login API as of April 14, 2025. Applications created after that date must use OAuth 2.0 tokens for authentication instead of User API Keys.

The flow follows a standard authorization code grant:

  1. Redirect the user to https://{companyDomain}.bamboohr.com/authorize.php with your client_id, redirect_uri, requested scopes, and response_type=code
  2. BambooHR redirects back with a temporary code
  3. Exchange that code at the token endpoint for an access_token and refresh_token
sequenceDiagram
    participant App as Your App
    participant Browser as User Browser
    participant BHR as BambooHR
    App->>Browser: Redirect to authorize.php
    Browser->>BHR: User logs in, grants consent
    BHR->>Browser: Redirect to callback with ?code=
    Browser->>App: Callback with auth code
    App->>BHR: POST /token.php (code + client_secret)
    BHR->>App: access_token + refresh_token
    App->>BHR: API requests with Bearer token

The access token expires in 3600 seconds. You'll only receive a refresh_token if you include the offline_access scope in your initial authorization request — miss that scope and you're stuck re-authorizing users every hour.

The Redirect URI is the URL you registered when creating your app in the BambooHR Developer Portal. During the OAuth flow, BambooHR redirects users to this address along with a temporary authorization code. Make sure the Redirect URI you use in your requests exactly matches the one you registered — even small differences (such as an extra slash or capitalization change) can cause authentication errors.

Tip

Which should you pick? API keys are fine for internal tooling where a single BambooHR admin owns the integration. For a B2B SaaS product connecting to your customers' BambooHR accounts, OAuth 2.0 is required. BambooHR is actively deprecating legacy auth paths — build on OAuth from day one.

Handling BambooHR API Pagination and Rate Limits

This is where BambooHR's "simple API" reputation starts to crack.

The New Cursor-Based Pagination Endpoint

BambooHR added a new public API endpoint, GET /api/v1/employees, which provides a more flexible and efficient way to retrieve employee data. This endpoint supports filtering, sorting, and pagination, allowing developers to request only the employees and fields they need.

Before this endpoint (added in late 2025), you had two options for bulk employee retrieval: the /employees/directory endpoint (which dumps everyone at once with limited fields) or custom reports (which require you to pre-define the fields you want). Neither supported proper cursor-based pagination.

The existing directory endpoint and custom reports remain available and unchanged. No action is required for current integrations; however, developers may review the new endpoint to determine whether its capabilities better align with their use cases.

The 400-Field Limit

BambooHR set a limit of up to 400 fields that can be requested for a single request to the Get Employee endpoint. This limit is being applied to help ensure the stability and performance of their systems for everyone.

BambooHR is also researching a reasonable limit to the number of fields that can be requested in a single custom report. 90% of uses of the Request Custom Report endpoint request 30 or less fields. However there are some requests that attempt to get 1000 or more fields in a single request.

If your integration needs more than 400 fields per employee (common in companies with many custom fields), you'll need to batch your field requests across multiple API calls and merge the results client-side. For a complex analytics product requiring 600 fields, that means executing multiple paginated passes and stitching the JSON payloads in memory before writing to your database.

Rate Limiting: The Undocumented Problem

Here's where things get frustrating. BambooHR does have rate limits, but specific details are not easily found in the public documentation. API requests can be throttled if BambooHR deems them to be too frequent. Implementations should always be prepared for a 503 Service Unavailable response.

When rate limiting occurs, a "Retry-After" header may be available in the response, indicating when it's appropriate to retry the request. That "may be" is doing a lot of heavy lifting — you can't rely on it being there.

When you hit their limits, the API throws specific HTTP status codes:

  • 429 Limit Exceeded: You're making too many requests per second.
  • 503 Service Unavailable: The BambooHR gateway is overwhelmed and dropping connections.

If your integration relies on a naive while loop to fetch data, a 429 error will crash your sync job. Here's a production-grade retry pattern:

import time
import random
import requests
from requests.auth import HTTPBasicAuth
 
def bamboo_request(url, api_key, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(
            url,
            auth=HTTPBasicAuth(api_key, "x"),
            headers={"Accept": "application/json"}
        )
        
        if response.status_code == 200:
            return response.json()
        
        if response.status_code in (429, 503):
            retry_after = response.headers.get("Retry-After")
            if retry_after:
                wait = int(retry_after)
            else:
                # Exponential backoff with jitter
                wait = min(2 ** attempt + random.uniform(0, 1), 60)
            
            time.sleep(wait)
            continue
        
        response.raise_for_status()
    
    raise Exception(f"Max retries exceeded for {url}")

Here's what a typical sync flow looks like when backoff kicks in:

sequenceDiagram
    participant SaaS as Your Application
    participant Worker as Background Job
    participant Bamboo as BambooHR API
    
    SaaS->>Worker: Trigger Directory Sync
    Worker->>Bamboo: GET /v1/employees?cursor=null
    Bamboo-->>Worker: HTTP 200 (Page 1)
    Worker->>Bamboo: GET /v1/employees?cursor=xyz123
    Bamboo-->>Worker: HTTP 429 Limit Exceeded
    Note over Worker: Sleep for 2 seconds<br>Retry request
    Worker->>Bamboo: GET /v1/employees?cursor=xyz123
    Bamboo-->>Worker: HTTP 503 Service Unavailable
    Note over Worker: Sleep for 4 seconds<br>Retry request
    Worker->>Bamboo: GET /v1/employees?cursor=xyz123
    Bamboo-->>Worker: HTTP 200 (Page 2)
Info

BambooHR's rate limits are intentionally undocumented and subject to change at their discretion. BambooHR may, in its sole discretion, limit, modify, suspend, or discontinue access to any Developer Tools or specific API endpoints at any time, including by imposing or adjusting rate limits. Build your integration assuming limits will tighten over time.

For a deeper look at handling rate limits across multiple HR platforms, see our guide to API rate limit best practices.

Webhooks vs. Polling for Real-Time Sync

To keep your application updated when an employee is hired or terminated, you have two choices: poll the API on a cron schedule, or listen for webhooks.

BambooHR has made meaningful improvements here. They transitioned from a cron-based system to a real-time event-driven architecture. With this transition, they removed webhook scheduling and rate limiting features from the user interface as they are no longer needed for real-time delivery. If your webhook configuration used scheduling or rate limiting features, your webhooks automatically transitioned to real-time delivery with no action required.

They also expanded webhook functionality to include support for custom fields, allowing customers and partners to monitor and receive real-time notifications when their own custom data changes.

That said, relying purely on webhooks introduces operational overhead:

  1. Delivery guarantees. If your receiving endpoint goes down for maintenance, payloads may be dropped. You need a highly available queue immediately behind your webhook receiver to ensure no data is lost.
  2. Idempotency. Duplicate events happen. Your processing logic needs to handle receiving the same event multiple times without corrupting state.
  3. Manual configuration friction. BambooHR webhooks often require the customer's HR admin to log into their dashboard and manually configure the endpoint URL and select trigger fields. This creates friction during onboarding.

Because of these realities, most engineering teams implement a hybrid approach: webhooks for real-time updates, plus a nightly polling job to catch missed events and ensure absolute data consistency.

The Hidden Costs of Building a Native BambooHR Integration

Let's do the math that sprint planning never does.

Week 1-2: Your engineer builds the happy path. Basic auth, pull employees, map fields. Demo looks great. Ship it.

Month 2: A customer reports missing custom fields. Turns out the API key they generated doesn't have admin permissions. Debugging takes a day because the API just silently omits fields the key can't access — no error, no warning.

Month 4: BambooHR pushes a change to their webhook system. Some fields your integration relied on are removed. BambooHR removed some webhook fields — some no longer exist in their database, while others had extremely low usage. They reached out directly to impacted customers, but your customer didn't forward the notice to your engineering team.

Month 8: Your second enterprise customer uses BambooHR with 2,000 employees and 200 custom fields. Your integration hits the 400-field limit and starts silently dropping data. The pagination approach that worked for a 50-person company now triggers rate limits.

This pattern repeats across every HRIS vendor, not just BambooHR—and we see the exact same maintenance burden when teams build accounting integrations for QuickBooks, FreshBooks, or Xero. Organizations need $50,000 to $150,000 yearly to cover staff and partnership fees for ongoing API integration maintenance, according to research by Netguru. That's not the cost of building — that's the cost of keeping it running.

Cost Category Estimated Annual Cost
Initial build (amortized) $15,000 – $30,000
Ongoing maintenance & monitoring $20,000 – $50,000
Edge case debugging $10,000 – $25,000
Auth changes & API deprecation handling $5,000 – $15,000
Customer support escalations $5,000 – $15,000
Total (single vendor) $55,000 – $135,000

And that's for one HRIS provider. Most B2B SaaS products need to support BambooHR, Workday, ADP, Gusto, Rippling, and a half-dozen others. Multiply accordingly.

Every hour spent babysitting third-party HR APIs is an hour not spent on the core product your customers actually pay for. For a detailed breakdown, read our post on building native HRIS integrations without draining engineering.

How Truto Handles BambooHR (and 50+ Other HRIS Providers)

Let's be honest about trade-offs. A unified API adds an abstraction layer between you and the vendor API. That means less raw control, and there will be edge cases where you need data outside the unified schema. Any vendor who tells you their unified API covers 100% of every provider's surface area is lying to you.

That said, here's what Truto's architecture actually does for BambooHR integrations:

Zero integration-specific code. You don't write code specific to BambooHR — you write code against the Truto Unified Schema. When you request an employee directory, you hit the Truto /unified/hris/employees endpoint. The platform automatically translates that request into BambooHR's native format, handles the 400-field batching, manages cursor-based pagination, and returns a clean, standardized JSON array.

If the customer uses Workday instead of BambooHR, your code doesn't change. Truto routes the request to the correct provider using the integrated_account_id parameter.

curl -H "Authorization: Bearer {TRUTO_TOKEN}" \
  "https://api.truto.one/unified/hris/employees?integrated_account_id={ACCOUNT_ID}"

The unified data model. Instead of BambooHR's field-name-based schema, you work with standardized entities:

erDiagram
    COMPANY ||--o{ LOCATION : has
    COMPANY ||--o{ GROUP : has
    COMPANY ||--o{ JOB_ROLE : defines
    EMPLOYEE }o--|| COMPANY : belongs_to
    EMPLOYEE ||--o{ EMPLOYMENT : has
    EMPLOYEE }o--o{ GROUP : member_of
    EMPLOYEE }o--o| JOB_ROLE : assigned
    EMPLOYEE ||--o{ TIMEOFF_REQUEST : submits
    EMPLOYEE ||--o{ EMPLOYEE_COMPENSATION : receives

This schema holds whether you're pulling from BambooHR, Workday, or Personio.

Automatic auth lifecycle management. Truto manages both API key storage (encrypted at rest) and OAuth 2.0 token lifecycles. For OAuth-connected accounts, Truto refreshes tokens shortly before they expire. If a refresh fails, the account is flagged as needing re-authorization and your application is notified via webhook — no silent failures.

Pagination and rate limit handling. Truto's pagination system adapts to each provider's approach. For BambooHR, that means using cursor-based pagination on the new employees endpoint and handling the 400-field batching automatically. Rate limit responses trigger exponential backoff within the platform, so your application never sees a 429 or 503.

What Truto doesn't solve. If you need BambooHR-specific features outside the unified schema — like time tracking break policies or applicant tracking modules — you can use Truto's Proxy API to make direct calls to BambooHR's native endpoints. The Proxy API still gives you managed auth and rate limiting, but you're back to provider-specific response formats.

For companies that need to handle custom field mappings that differ between customers, Truto uses a three-level override hierarchy: platform defaults, environment-level overrides for your product, and per-account overrides for individual customers with unusual configurations.

Quick Start: Pull Users Across All Connected SaaS Apps

If you landed here because you need to pull a list of users from every SaaS app your customers use - BambooHR, Google Workspace, Okta, Microsoft Entra ID - without building each integration from scratch, this section is your starting point.

The pattern is the same whether you're pulling from one provider or twenty: authenticate with Truto, specify which connected account to query, and get back a normalized response.

Step 1: Authenticate with Truto

Every request requires a Bearer token (your Truto API key) and an integrated_account_id that identifies a specific customer's connected account - for example, "Acme Corp's BambooHR instance."

# All Truto requests use the same auth pattern
curl -H "Authorization: Bearer {TRUTO_TOKEN}" \
     -H "Accept: application/json" \
     "https://api.truto.one/unified/hris/employees?integrated_account_id={ACCOUNT_ID}"

No BambooHR API keys, no subdomain routing, no Basic Auth encoding. Truto handles the provider-specific authentication for the connected account.

Step 2: Read the Response

Truto always transforms any underlying pagination format to a cursor-based pagination format. Truto follows a single response format across all integrations, with unified cursor-based pagination and all data present in the result attribute.

{
  "result": [
    {
      "id": "truto_emp_8a3f",
      "remote_id": "100",
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane.doe@acme.com",
      "department": "Engineering",
      "job_title": "Staff Engineer",
      "employment_status": "active",
      "start_date": "2023-03-15",
      "termination_date": null,
      "manager": {
        "id": "truto_emp_00042",
        "remote_id": "42"
      },
      "created_at": "2023-03-15T08:00:00Z",
      "modified_at": "2026-01-10T14:22:00Z",
      "remote_data": {}
    }
  ],
  "next_cursor": "eyJwYWdlIjoyLCJsaW1pdCI6MTAwfQ=="
}

This response looks identical whether the source is BambooHR, Workday, Gusto, or any other supported HRIS. Swap the integrated_account_id and the schema stays the same.

Step 3: Pull from Identity Providers Too

For non-HRIS SaaS apps - directory services like Okta, Google Workspace, or Microsoft Entra ID - use the User Directory API:

curl -H "Authorization: Bearer {TRUTO_TOKEN}" \
     -H "Accept: application/json" \
     "https://api.truto.one/unified/user-directory/users?integrated_account_id={OKTA_ACCOUNT_ID}"

Same auth pattern, same pagination model, different unified schema optimized for identity and access data. Between the HRIS and User Directory APIs, you can cover the vast majority of SaaS apps where user records live.

Aggregating Users Across Multiple Providers

Here's the practical answer to "how do I pull users from all the SaaS apps my customers use?" The pattern is: iterate over every connected account for a customer and hit the same unified endpoint for each one.

import requests
 
TRUTO_TOKEN = "your_truto_api_token"
BASE = "https://api.truto.one"
HEADERS = {
    "Authorization": f"Bearer {TRUTO_TOKEN}",
    "Accept": "application/json"
}
 
# Each entry represents one of your customer's connected accounts.
# You store these when the customer links their SaaS apps via Truto's SDK.
connected_accounts = [
    {"id": "ia_bamboohr_acme",  "provider": "bamboohr",        "category": "hris"},
    {"id": "ia_okta_acme",      "provider": "okta",             "category": "user-directory"},
    {"id": "ia_gworkspace_acme","provider": "google-workspace", "category": "user-directory"},
]
 
all_users = []
 
for account in connected_accounts:
    # Pick the right unified endpoint based on category
    if account["category"] == "hris":
        endpoint = f"{BASE}/unified/hris/employees"
    else:
        endpoint = f"{BASE}/unified/user-directory/users"
 
    cursor = None
    while True:
        params = {"integrated_account_id": account["id"]}
        if cursor:
            params["cursor"] = cursor
 
        resp = requests.get(endpoint, headers=HEADERS, params=params)
        resp.raise_for_status()
        data = resp.json()
 
        for user in data["result"]:
            user["_source_provider"] = account["provider"]
            user["_source_account_id"] = account["id"]
            all_users.append(user)
 
        cursor = data.get("next_cursor")
        if not cursor:
            break
 
print(f"Pulled {len(all_users)} users across {len(connected_accounts)} providers")

The unified schema means you don't write provider-specific parsing logic. Every response maps to the same field names regardless of whether it came from BambooHR, Okta, or Google Workspace.

flowchart LR
    A[Your App] -->|Same Bearer token| B[Truto API]
    B -->|integrated_account_id=bamboohr| C[BambooHR]
    B -->|integrated_account_id=workday| D[Workday]
    B -->|integrated_account_id=okta| E[Okta]
    B -->|integrated_account_id=google| F[Google Workspace]
    C --> G[Unified JSON]
    D --> G
    E --> G
    F --> G
    G --> H[Your Database]

This works whether a customer has connected two providers or twenty. The integrated_account_id parameter is the only thing that changes between requests.

Pagination and Large Result Sets

When a customer has thousands of employees, results come back in pages. Every response includes a next_cursor field - pass it back as a query parameter to get the next page.

# First page
curl -H "Authorization: Bearer {TRUTO_TOKEN}" \
     "https://api.truto.one/unified/hris/employees?integrated_account_id={ACCOUNT_ID}&limit=100"
 
# Response includes: "next_cursor": "eyJwYWdlIjoyLCJsaW1pdCI6MTAwfQ=="
 
# Next page
curl -H "Authorization: Bearer {TRUTO_TOKEN}" \
     "https://api.truto.one/unified/hris/employees?integrated_account_id={ACCOUNT_ID}&limit=100&cursor=eyJwYWdlIjoyLCJsaW1pdCI6MTAwfQ=="

When next_cursor is null or absent, you've reached the end. This model is consistent across every provider. BambooHR uses cursor-based pagination natively on its newer endpoint, but other providers may use offset-based or page-number-based pagination internally - Truto normalizes all of them into the same cursor model on your side.

For the aggregation use case, paginate each provider fully before moving to the next. This keeps memory predictable and makes it straightforward to resume if a sync job is interrupted partway through.

Tip

For very large directories (10,000+ employees), fetching every record in real-time on every request can be slow and will consume the underlying provider's rate limit budget. Consider syncing to your own database on a schedule and serving reads from there. Truto's real-time pass-through is ideal for on-demand reads and incremental updates, but bulk initial loads benefit from a background sync pattern.

Error Handling and Retry Patterns

The Truto API uses standard HTTP status codes. Here's what you'll encounter and how to handle each:

Status Code Meaning Action
200 Success Process the response
400 Bad request (invalid params) Check query parameters and fix the request
401 Invalid or expired Truto token Refresh your Truto API token
404 Account or resource not found Verify the integrated_account_id is correct and active
422 Unprocessable entity The provider rejected the request - check field values
429 Rate limit exceeded Back off and retry (see below)
502/503 Upstream provider unavailable Retry with exponential backoff

A retry wrapper that handles transient failures:

import time
import random
import requests
 
def truto_request(url, headers, params, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.get(url, headers=headers, params=params)
 
        if resp.status_code == 200:
            return resp.json()
 
        if resp.status_code in (429, 502, 503):
            retry_after = resp.headers.get("Retry-After")
            wait = int(retry_after) if retry_after else min(2 ** attempt + random.uniform(0, 1), 60)
            time.sleep(wait)
            continue
 
        if resp.status_code == 401:
            raise Exception("Truto API token is invalid or expired")
 
        resp.raise_for_status()
 
    raise Exception(f"Max retries exceeded for {url}")

Truto relies on real-time data and does not cache it, which means it counts on you, the customer, to manage the rate limiting. Since Truto calls data in real time, you should keep your request rate within the underlying API's limit. Truto handles provider-level backoff (retrying when BambooHR returns a 429), but your application is responsible for not hammering the unified API faster than the underlying provider can handle.

Troubleshooting Common Issues

Symptom Likely Cause Fix
Empty result array The connected account has no records, or the connection is stale Verify the account is active in the Truto dashboard. Re-authorize if needed.
401 Unauthorized Your Truto API token is expired or invalid Generate a new API token from the Truto dashboard
404 Not Found The integrated_account_id doesn't exist or was deleted Confirm the account ID is correct and the connection is still live
Fields returning null The source provider doesn't supply that field, or the connected user lacks permission Check the remote_data field to see what the provider actually returned. Use the Proxy API for direct access if needed.
Slow responses on large directories The underlying provider is paginating through thousands of records in real-time Use smaller page sizes via the limit parameter. For bulk loads, sync to your own database on a schedule.

Sample Response and Field Reference

Here's a complete response from the unified HRIS employees endpoint. This format is identical whether the source is BambooHR, Workday, Personio, or any other connected provider:

{
  "result": [
    {
      "id": "truto_emp_8a3f",
      "remote_id": "100",
      "first_name": "Jane",
      "last_name": "Doe",
      "display_name": "Jane Doe",
      "email": "jane.doe@acme.com",
      "personal_email": "jane@personal.com",
      "department": "Engineering",
      "job_title": "Staff Engineer",
      "employment_status": "active",
      "employment_type": "full_time",
      "start_date": "2023-03-15",
      "termination_date": null,
      "work_location": "San Francisco",
      "manager": {
        "id": "truto_emp_00042",
        "remote_id": "42"
      },
      "groups": [
        {
          "id": "grp_eng",
          "name": "Engineering",
          "type": "department"
        }
      ],
      "created_at": "2023-03-15T08:00:00Z",
      "modified_at": "2026-01-10T14:22:00Z",
      "remote_data": {}
    }
  ],
  "next_cursor": "eyJwYWdlIjoyLCJsaW1pdCI6MTAwfQ=="
}

Key fields:

  • id - Truto's stable identifier for the record across syncs.
  • remote_id - The employee's ID in the source provider (e.g., BambooHR's numeric employee ID). Use this when cross-referencing with the native system.
  • employment_status - Represents the employment status. If no clear mapping is available, then the raw value is returned. Normalized values include active, inactive, terminated, and on_leave where possible.
  • manager - Represents the manager of the employee. Is also an employee. Returned as a nested object with its own id and remote_id.
  • remote_data - Raw data returned from the remote API call. When you need a BambooHR-specific field that isn't in the unified schema, check here before falling back to the Proxy API.
  • groups - Type of the group. Some underlying providers use this to differentiate between built-in and user-created groups.

Data Residency and Zero-Cache Behavior

Security teams evaluating a unified API need clear answers on where employee PII lives. Here's how Truto handles it:

Pass-through architecture. Instead of caching third-party data on its servers, Truto operates as a real-time pass-through proxy - every API call flows directly to the source system, gets transformed on the fly, and returns to your application without Truto storing the payload. Employee names, emails, compensation data, and other PII are processed in memory during the request-response cycle and never written to persistent storage.

What this means for compliance. By using an integration tool with a pass-through architecture, you bypass this trap entirely. Because the middleware does not store the data, it's classified as a conduit rather than a data custodian. This simplifies your data processing agreements and vendor risk assessments.

Credentials at rest. OAuth tokens and API keys for connected accounts are encrypted at rest. Tokens are decrypted only in memory at the moment the API request is dispatched.

Compliance certifications. Truto is SOC 2 Type II and ISO 27001 compliant, GDPR and HIPAA certified. The pass-through architecture makes compliance reviews with enterprise security teams dramatically simpler.

Deployment options. You can choose between Truto Cloud and Truto on-prem. For organizations that need the API layer to run entirely within their own infrastructure, the on-prem option keeps all data transit within your network boundary.

The honest trade-off. Pass-through architectures are slower than cached ones. Walking through 50 pages of a third-party API on every request adds real latency. If you need sub-100ms response times on integration data, you'll need to cache data in your own infrastructure where you control retention, encryption, and residency. The pass-through model gives you the cleanest compliance posture, but for high-frequency read patterns, plan to sync data into your own database on a schedule and serve reads from there.

What Should You Actually Do?

Here's the decision framework:

Build it yourself if:

  • BambooHR is the only HRIS you'll ever support (and you're certain about that)
  • You need deep access to BambooHR-specific features like time tracking break policies or benefits administration
  • Your team has spare capacity and you want full control over the data pipeline

Use a unified API if:

  • Your sales team is already asking for Workday, ADP, and Gusto support alongside BambooHR
  • You'd rather spend engineering cycles on your core product
  • You need to move fast — integrating through a unified API takes days, not quarters

BambooHR's API is one of the friendlier HRIS APIs out there. The docs are decent (by HR vendor standards), the data model is flat, and the REST conventions are mostly standard. But "friendlier" doesn't mean "free to maintain." The auth migration alone — from API keys to OAuth 2.0, with the OpenID Connect deprecation thrown in — is a reminder that even the simplest vendor APIs change underneath you.

You have to handle Basic Auth with API keys securely, navigate the strict 400-field limit, build cursor-based pagination loops, and implement exponential backoff for undocumented rate limits. While building this in-house is entirely possible, the ongoing maintenance burden makes it a poor use of highly paid engineering talent the moment you need more than one provider. And in HRIS, you always need more than one provider.

If you're weighing the build vs. buy decision for integrations, the math usually tips toward buy somewhere around provider number two. To evaluate how different platforms handle this architecture, review our comparison of alternatives for HRIS integrations.

FAQ

Can I pull a list of users from all my customers' SaaS apps without building each integration separately?
Yes. A unified API lets you query a single endpoint per software category (HRIS, User Directory) and swap the integrated_account_id parameter to pull users from BambooHR, Okta, Google Workspace, or any other connected provider. The response schema is the same regardless of the source, so you write one integration and aggregate across all connected apps.
Does Truto cache or store my customers' employee data?
No. Truto operates as a real-time pass-through proxy. Every API call flows directly to the source system, gets transformed in memory, and returns to your application. Employee PII is never written to persistent storage on Truto's infrastructure. Truto is SOC 2 Type II and ISO 27001 compliant, and GDPR and HIPAA certified.
What are the main gotchas when integrating directly with the BambooHR API?
The biggest surprises are undocumented rate limits (you'll get 429 or 503 responses with no published thresholds), a 400-field-per-request ceiling that silently drops data for companies with many custom fields, API key lockout if you retry with an invalid key, and the OAuth 2.0 migration that makes API keys insufficient for new marketplace apps as of April 2025.
How does pagination work when pulling users through a unified API?
Truto normalizes every provider's native pagination (offset, page-number, or cursor) into a consistent cursor-based model. Each response includes a next_cursor field. Pass it back as a query parameter to get the next page. When next_cursor is null or absent, you've reached the end.

More from our Blog