Skip to content

BambooHR to Lever Migration: The CTO's Technical Guide

A technical guide to migrating from BambooHR to Lever. Learn how to map opportunity-centric data models, bypass API limits, and ensure zero data loss.

Roopendra Talekar Roopendra Talekar · · 11 min read
BambooHR to Lever Migration: The CTO's Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Migrating from BambooHR's built-in ATS to Lever is a data-model translation problem disguised as a vendor switch. BambooHR is an HRIS with a functional, application-centric ATS. It handles inbound applicants well but lacks proactive sourcing tools, CRM features, and advanced pipeline customization. When scaling engineering and talent teams move to Lever, they are adopting an opportunity-centric data model.

A naive CSV export from BambooHR flattens this relational structure. It drops historical interview notes, breaks multi-application candidate histories, and collapses the candidate lifecycle into unusable rows.

This guide covers the object-mapping decisions you must make, the API constraints that will bottleneck your ETL scripts, and the edge cases that break most DIY migration attempts.

The Architectural Shift: Application-Centric vs. Opportunity-Centric Data Models

Before writing a single line of extraction code, you must understand how both systems store talent data.

BambooHR's ATS uses a flat, application-centric model. A candidate applies, and their record is tied directly to that application. If a candidate applies to three different roles over two years, BambooHR treats these as separate application records under the /applicant_tracking/applications endpoint. There is no first-class "candidate" object that unifies them — the application is the primary entity.

Lever utilizes an opportunity-centric data model. Every candidate in your Lever environment has exactly one Candidate Profile. That single profile acts as a central source of truth for contact information. When a candidate applies for a role, Lever creates an Opportunity — representing the consideration of that candidate for a specific job.

Moving data between these systems requires splitting BambooHR's flat application records into Lever's distinct Candidate and Opportunity objects.

Evaluating Migration Approaches: CSV vs. API vs. Middleware

You have three primary paths for moving your historical candidate data.

Native CSV Export/Import

BambooHR allows you to export standard reports to CSV, which you can then format for Lever's bulk import tool.

  • How it works: You export applicant data from BambooHR, map the columns in a spreadsheet to Lever's required fields, and upload the file.
  • When to use it: Small datasets (under 1,000 records) where historical context does not matter.
  • Pros and cons: It is free and requires no engineering bandwidth. However, it is highly manual and prone to breaking relational data. BambooHR's native exports miss candidate notes, interview feedback, and file attachments — the CSV contains only the fields available in the standard applicant report.
  • Complexity: Low.

For a deeper dive into why flat files fail relational data, see our guide on using CSVs for SaaS data migrations.

Middleware (Zapier, Make)

Integration platforms are built for point-to-point triggers, not historical bulk data movement.

  • How it works: You set up a trigger (e.g., "New Applicant in BambooHR") and an action ("Create Opportunity in Lever").
  • When to use it: Excellent for ongoing syncs — such as syncing hired Lever candidates back to BambooHR to create an employee record.
  • Pros and cons: Great for forward-looking automation. Not designed for historical data. Middleware lacks the architecture to handle bulk pagination, complex data transformations, and historical timestamp preservation.
  • Complexity: Medium.

DIY Custom ETL Scripts

Building a custom extraction, transformation, and load (ETL) pipeline using Python or Node.js.

  • How it works: Your script authenticates with BambooHR's API, paginates through all historical records, transforms the JSON payloads into Lever's schema, and POSTs them to Lever's API.
  • When to use it: When you have dedicated engineering bandwidth and strict requirements for data completeness.
  • Pros and cons: Offers total control over the data mapping. The downside is the engineering cost. You must handle pagination, API key authentication (BambooHR) and API key or OAuth (Lever, depending on the endpoint), and strict rate limits without dropping records.
  • Complexity: High.

Pre-Migration Planning & Data Mapping Strategy

A successful migration requires mapping BambooHR's rigid statuses to Lever's customizable pipeline stages.

  1. Audit your data: Identify active candidates, rejected applicants, hired employees, and archived jobs.
  2. Define the scope: Decide if you are migrating all historical data or only candidates active in the last 24 months.
  3. Map the fields: Document exactly how BambooHR fields translate to Lever.

Sample Data Mapping Table

BambooHR Object/Field Lever Equivalent Notes
Candidate Name (firstName, lastName) Contact: name Lever uses a single concatenated string.
Email Contact: emails [] Used as the deduplication key when checking for existing Candidate Profiles.
Phone Contact: phones [].value Include type ("mobile", "home") if available in source.
Job Title Opportunity: headline Extracted from the BambooHR application record.
Status (New, Accepted, Declined, etc.) Opportunity: stage Requires explicit mapping — see stage mapping section below.
Resume Attachment Opportunity: files Must be downloaded from BambooHR via API and uploaded to Lever as a multipart file.
Candidate Notes Profile: notes BambooHR's /applicant_tracking/applications/{id}/comments endpoint returns interviewer comments, but the response does not include the author's user ID in all cases.
Source / Referral Opportunity: sources [] Map BambooHR's source field to Lever's source tag array.
Custom Fields Opportunity: custom fields via tags or Lever's custom fields API BambooHR custom fields require querying /meta/lists/ to resolve dropdown IDs to display values before mapping.
EEO Data Lever: EEO fields (if enabled) Lever's EEO collection is handled separately and may not accept imported values depending on your Lever plan and compliance settings. Confirm with Lever support.
Application Date (dateCreated) Opportunity: custom field Lever's API does not allow backdating the createdAt timestamp on opportunities. Store the original BambooHR application date in a custom field to preserve historical accuracy for reporting.
Tags Opportunity: tags [] Direct string mapping.
Archive Status Opportunity: archive reason Map BambooHR's terminal statuses (Declined, Withdrawn) to Lever archive reasons via POST /opportunities/{id}/archived.

Stage Mapping: BambooHR Statuses → Lever Pipeline Stages

BambooHR uses a fixed set of application statuses. Lever uses fully customizable pipeline stages that vary per posting. You need to define explicit mapping rules before writing any transformation code.

BambooHR Status Suggested Lever Stage Decision Logic
New New Lead / New Applicant Direct map to the top of your Lever pipeline.
Reviewed Recruiter Screen Indicates a recruiter looked at the application.
Phone Screen Phone Screen Direct map if your Lever pipeline has this stage.
Interview On-Site / Technical Interview Map to whichever interview stage is appropriate. If Lever has multiple interview stages, default to the first.
Offer Offer Direct map.
Hired Hired (archived) Archive the opportunity with reason "Hired."
Declined Archived Archive with reason "Candidate declined."
Withdrawn Archived Archive with reason "Candidate withdrew."
On Hold Custom stage or tag Lever has no native "On Hold" stage. Create a custom stage, or archive with a tag indicating hold status. Document your decision — this is an ambiguous state that your recruiting team must sign off on.
Warning

Do not skip the stage mapping review with your talent team. BambooHR statuses like "On Hold" have no clean Lever equivalent. If you default these to "Archived," you may lose candidates your recruiters intended to revisit.

When handling sensitive candidate data, ensure your extraction methods comply with local regulations. Review our guide on GDPR & CCPA compliance during candidate data transfers. In practice, this means your extraction scripts should log which records were accessed and transferred, and your migration scope should exclude candidates who have exercised data deletion rights.

Migration Architecture & Step-by-Step Execution

If you choose to build a custom ETL pipeline, follow this architectural flow.

1. Extract from BambooHR

BambooHR's API uses basic HTTP authentication where the API key acts as the username (password is any arbitrary string, typically "x"). You will need to query the /applicant_tracking/applications endpoint to pull candidate data. Because BambooHR paginates its responses, your script must handle offset logic to retrieve the entire dataset.

To extract candidate comments and interview notes, query /applicant_tracking/applications/{id}/comments per application. This endpoint returns comment text and timestamps but may omit the commenter's identity in some cases. File attachments (resumes, cover letters) require a separate download call per file.

2. Transform the Data Model

You must split the BambooHR payload. Extract the candidate's personal information (email, phone, name) to build the Lever Candidate Profile payload. Extract the job-specific information (job ID, status, resume) to build the Lever Opportunity payload.

Deduplication is the critical step. Before creating a Candidate Profile in Lever, query GET /candidates with the candidate's email address. If a profile already exists (because the candidate applied to multiple roles), append the new Opportunity to the existing profile using POST /opportunities with the contact ID. If no profile exists, create both simultaneously.

Timestamp preservation: Lever's API sets createdAt to the current time on all new records — you cannot backdate it. To preserve the original BambooHR application date, write it into a custom field on the Opportunity (e.g., original_application_date). Without this, any historical reporting on time-to-hire or pipeline velocity will be inaccurate.

3. Load into Lever

Lever's API uses API key authentication for most endpoints (Authorization: Basic {base64(api_key:)}) but requires OAuth for partner integrations. For a one-time migration using your own Lever API key, basic auth is sufficient.

You must first check if a Candidate Profile exists using the candidate's email. If it does, append the new Opportunity to the existing profile. If it does not, create the profile and the opportunity simultaneously.

For more context on Lever's API behaviors, see our Greenhouse to Lever migration guide.

Pseudo-Code Example

import requests
import time
 
BAMBOOHR_URL = "https://api.bamboohr.com/api/gateway.php/{company}/v1/applicant_tracking/applications"
LEVER_URL = "https://api.lever.co/v1/opportunities"
LEVER_CANDIDATES_URL = "https://api.lever.co/v1/candidates"
 
# Extract from BambooHR
response = requests.get(
    BAMBOOHR_URL,
    auth=(BAMBOOHR_API_KEY, "x"),
    headers={"Accept": "application/json"}
)
applications = response.json()
 
# Transform and Load into Lever
for app in applications:
    email = app.get('email')
    
    # Check for existing candidate profile (deduplication)
    existing = requests.get(
        LEVER_CANDIDATES_URL,
        auth=(LEVER_API_KEY, ""),
        params={"email": email}
    )
    
    candidate_id = None
    if existing.status_code == 200 and existing.json().get('data'):
        candidate_id = existing.json()['data'][0]['id']
    
    payload = {
        "name": f"{app['firstName']} {app['lastName']}",
        "headline": app.get('jobTitle'),
        "emails": [email],
        "phones": [{"value": app.get('phone')}],
        "tags": ["migrated-from-bamboohr"],
        "sources": [app.get('source', 'BambooHR Import')]
    }
    
    if candidate_id:
        payload["contact"] = candidate_id
    
    # Lever limits POST requests to 2 per second
    res = requests.post(
        LEVER_URL,
        auth=(LEVER_API_KEY, ""),
        json=payload
    )
    
    if res.status_code == 429:
        retry_after = int(res.headers.get('Retry-After', 2))
        print(f"Rate limit hit. Sleeping {retry_after}s.")
        time.sleep(retry_after)
        # Retry the same request
        res = requests.post(
            LEVER_URL,
            auth=(LEVER_API_KEY, ""),
            json=payload
        )
    elif res.status_code == 409:
        print(f"Conflict on {email} — duplicate detected by Lever.")
        # Log and continue; do not retry
    elif res.status_code == 422:
        print(f"Validation error for {email}: {res.json()}")
        # Log the full response body — common causes:
        # missing required field, malformed email, invalid stage ID
        
    # Enforce rate limit natively
    time.sleep(0.6) 

Handling Edge Cases, API Limits, & Constraints

Migrations fail in the edge cases. Here is what will break your scripts if you do not account for it.

Lever's Strict API Rate Limits

Lever strictly rate-limits application POST requests to 2 per second. If your migration script exceeds this, Lever returns a 429 TOO MANY REQUESTS status code with a Retry-After header. Your pipeline must include intelligent queueing, exponential backoff, and retry logic to prevent silent data loss during bulk uploads.

Common HTTP Errors During Migration

HTTP Status Lever Context Handling
429 Rate limit exceeded Read Retry-After header. Sleep and retry. Do not skip the record.
409 Duplicate candidate detected (email match) Log the conflict. Retrieve the existing candidate ID and append the Opportunity to it instead.
422 Validation failure The payload is malformed — missing required fields, invalid email format, or referencing a stage ID that does not exist. Log the full response body and the source record for manual review.
401 Authentication failure API key is invalid or expired. Stop the pipeline — do not continue, as every subsequent request will also fail.
500 Lever server error Retry with exponential backoff up to 3 attempts. If it persists, pause and escalate.

BambooHR API Extraction Constraints

Extracting historical data from BambooHR is not always straightforward. The /applicant_tracking/applications endpoint returns application-level data, but candidate comments require per-application calls to /applicant_tracking/applications/{id}/comments. File attachments (resumes, offer letters) require separate download requests. There is no single endpoint that returns a complete candidate record with all associated data.

Plan your extraction to make three passes: applications, then comments per application, then file downloads per application. For a dataset of 10,000 applications with an average of 2 comments each, this means roughly 30,000 API calls to BambooHR before you begin loading into Lever.

Multi-Level Relationships

If a candidate applied to three jobs, they should have one Lever Candidate Profile and three Lever Opportunities. If your script creates three separate Candidate Profiles, you will break Lever's reporting and force your recruiters to manually merge duplicate profiles.

The deduplication check (query by email before creating a profile) is not optional — it is the single most important step in the load phase. Getting this data model right is also critical for future portability — as we detail in our Lever to Greenhouse migration guide, improperly merged Lever records are notoriously difficult to untangle later.

Timestamp Drift

Both BambooHR and Lever store timestamps in UTC, but Lever's API does not accept a custom createdAt value on new records. Every opportunity you create will show today's date as the creation date. If your recruiting team runs reports on time-in-stage, time-to-hire, or source effectiveness, those reports will be meaningless without the original dates.

Workaround: Store the original BambooHR dateCreated value in a Lever custom field. Instruct your BI team to use this field for historical reporting instead of the Lever-native createdAt.

For a broader look at common pitfalls, read 5 "Gotchas" in ATS Migration.

Validation, Testing, & Post-Migration Tasks

Do not run a migration without a rollback plan.

  1. Run a sandbox test: Push 5% of your BambooHR data into a Lever sandbox environment. Contact Lever support to provision a sandbox if you do not already have one — this is typically available on Professional and Enterprise plans.
  2. Record count validation: Compare the number of BambooHR applications against the number of Lever opportunities created. Compare unique candidate emails against unique Lever Candidate Profiles. The opportunity count should match applications; the profile count should be less than or equal to applications (due to deduplication).
  3. Field-level validation: Spot-check 50 random candidates to verify resumes attached correctly, notes transferred with correct timestamps, stage assignments match your mapping table, and custom fields populated as expected.
  4. Deduplication audit: Search Lever for any candidate email that appeared in multiple BambooHR applications. Verify they have one profile with multiple opportunities, not multiple profiles.
  5. Rebuild automations: Once the data is live, rebuild your Slack notifications, interview scheduling triggers, and HRIS syncs in Lever.

When to Use a Managed Migration Service

Building an ETL pipeline for a one-time migration is a significant engineering investment. You will spend weeks reading API documentation, handling authentication, writing retry logic for 429 errors, and mapping custom fields — for code you will use once.

The decision to build in-house vs. outsource depends on your team's available bandwidth, the size of your dataset, and how much historical context you need to preserve. For datasets under 1,000 records with no custom fields, a CSV approach may be sufficient. For anything larger or more complex, the engineering cost of a custom pipeline typically exceeds the cost of a managed service.

Frequently Asked Questions

How do I export candidate notes from BambooHR?
BambooHR's native CSV export has limitations and often drops historical candidate notes. You must use the BambooHR API to extract complete candidate histories and attachments programmatically.
What is the Lever API rate limit for creating opportunities?
Lever strictly rate-limits application POST requests to 2 per second. Exceeding this limit will result in a 429 TOO MANY REQUESTS error, requiring your migration script to use exponential backoff.
How does Lever's data model differ from BambooHR?
BambooHR uses a flat, application-centric model where candidates are tied directly to specific jobs. Lever uses an opportunity-centric model, where a single candidate profile can have multiple distinct opportunities attached to it.

More from our Blog