---
title: "Bullhorn to Ceipal Migration: The CTO's Technical Guide (2026)"
slug: bullhorn-to-ceipal-migration-the-ctos-technical-guide-2026
date: 2026-04-23
author: Raaj
categories: [Migration Guide, Ceipal, Bullhorn]
excerpt: "A technical guide to migrating from Bullhorn to Ceipal — covering API rate limits, object mapping, ETL architecture, and edge cases that break ATS migrations."
tldr: "Bullhorn-to-Ceipal migration requires API-based extraction (1,500 req/min limit), careful object mapping (Application V2 has no Ceipal equivalent), and strict load ordering to preserve relationships."
canonical: https://clonepartner.com/blog/bullhorn-to-ceipal-migration-the-ctos-technical-guide-2026/
---

# Bullhorn to Ceipal Migration: The CTO's Technical Guide (2026)


Migrating from Bullhorn to Ceipal is a data-model translation problem. Bullhorn stores candidate and placement data across a deeply relational schema — `Candidate`, `ClientCorporation`, `ClientContact`, `JobOrder`, `JobSubmission`, `Placement` — connected through foreign keys and junction objects like `Application V2` and `Application History`. Ceipal uses a flatter, unified ATS/VMS/WFM schema organized around `Applicants`, `Clients`, `Job Postings`, `Submissions`, and `Placements`. Every architectural mismatch between these two systems is where candidate records, placement histories, and recruiter notes silently break.

If you need a fast decision: **A CSV export from Bullhorn will move rows but will flatten relationships, lose attachment binaries, and strip stage history from Application V2 records.** API-based extraction via Bullhorn's REST API, combined with Ceipal's API for loading, is the only path that preserves full-fidelity relational data at scale. But Bullhorn caps you at 1,500 API requests per minute, and Ceipal's auth tokens expire every hour — so any real migration pipeline needs retry logic, token refresh, and pagination built in from day one. ([kb.bullhorn.com](https://kb.bullhorn.com/ats/Content/BHATS/Topics/understandingBHAPIUsageLimitsVersioningBackwardCompatibility.htm))

This guide covers the real API constraints on both sides, object-by-object mapping, every viable migration method with trade-offs, and the edge cases that cause silent data corruption in ATS migrations.

For broader ATS migration patterns, see [5 "Gotchas" in ATS Migration](https://clonepartner.com/blog/blog/ats-migration-gotchas/). For why CSVs break complex data, see [Using CSVs for SaaS Data Migrations: Pros and Cons](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

## The Architectural Shift: Bullhorn vs. Ceipal Data Models

Understanding the structural gap is the prerequisite for every migration decision.

**Bullhorn** uses a relational entity model designed for staffing workflows. The core entities are:

- **Candidate** — job seekers with resumes, skills, and work history
- **ClientCorporation** — companies that post jobs
- **ClientContact** — people at those companies (hiring managers, HR)
- **JobOrder** — open positions linked to a ClientCorporation and ClientContact
- **JobSubmission** — the junction between Candidate and JobOrder (the "submittal")
- **Placement** — a filled position linking Candidate, JobOrder, and billing details
- **Opportunity** — potential deals, convertible to JobOrders
- **Note** — activity records tied to any entity

Bullhorn's ATS v2 data model adds the **Application V2** object and **Application History** to track a candidate's journey through customizable stages (Application → Submittal → Send Out → Offer → Closing Report). Each stage transition creates an Application History record, preserving the full audit trail. ([kb.bullhorn.com](https://kb.bullhorn.com/bh4sf/Content/BH4SF/Topics/ATSV2DataModel.htm))

**Ceipal's** data model is organized around:

- **Applicants** — equivalent to Bullhorn's Candidates
- **Clients** — equivalent to ClientCorporation
- **Leads** — sales pipeline contacts
- **Job Postings** — equivalent to JobOrders
- **Submissions** — equivalent to JobSubmissions
- **Placements** — similar concept but with different field structures
- **Vendors** — for VMS workflows (no direct Bullhorn equivalent in standard ATS)
- **Talent Bench** — bench candidates (no direct Bullhorn equivalent)

The structural mismatch creates specific migration risks:

| Area | Bullhorn | Ceipal | Migration Impact |
|---|---|---|---|
| Candidate flow | Candidate + `Application V2` + `Application History` | Applicant + Submission + Interview + Placement | One Bullhorn stage chain becomes several Ceipal records |
| Sales/company data | ClientCorporation, ClientContact, Lead, Opportunity | Client, Client Contact, Lead | Opportunity data needs redesign or external retention |
| Activities | Note, Task, Appointment | Notes/activity surfaces plus interview/submission entities | Activity parity is not 1:1 |
| Files/customization | File attachments, parsed resumes, custom objects | Documents, `resume_path`, fixed resources + Custom API | Files and unsupported source objects need explicit handling |

> [!WARNING]
> Ceipal does not expose a direct equivalent to Bullhorn's Application V2 / Application History objects through its API. Stage-level history and audit trails from Bullhorn's ATS v2 model cannot be mapped 1:1 into Ceipal. Plan for data loss or custom workarounds here.

Bullhorn supports **custom objects** that extend Candidate, ClientContact, ClientCorporation, JobOrder, Opportunity, and Placement entities. Ceipal supports custom fields within its standard objects but does not offer the same level of custom object extensibility. Ceipal's public docs expose a fixed resource set and point to a **Custom API** for additional fields. Any data stored in Bullhorn custom objects needs to be flattened into Ceipal's custom fields or accepted as out-of-scope. ([bullhorn.github.io](https://bullhorn.github.io/Custom-Objects/))

## Bullhorn API Constraints

Bullhorn's REST API is the primary extraction path. Here are the hard limits:

| Constraint | Limit |
|---|---|
| **Requests per minute** | 1,500 per OAuth Client ID |
| **Concurrent sessions (Enterprise)** | 50 maximum |
| **Concurrent sessions (Corporate)** | 25 maximum |
| **Monthly API calls** | 100,000 (unless negotiated) |
| **Active API subscriptions** | 50 |
| **Unused subscription TTL** | 30 days |
| **Rate limit error** | HTTP 429 Too Many Requests |

Bullhorn's guidance for handling 429 errors: wait 1 second, retry. Calls that return 429 do **not** count against your limits. Most clear within 10 retries. There's no need to pre-throttle — build proper retry logic instead. ([kb.bullhorn.com](https://kb.bullhorn.com/ats/Content/BHATS/Topics/understandingBHAPIUsageLimitsVersioningBackwardCompatibility.htm))

> [!NOTE]
> API usage by validated Bullhorn Marketplace Partners does not count toward your API call limits. If you're using a migration partner with Bullhorn partner status, their calls won't eat into your 100,000 monthly cap.

**Bullhorn ATS Growth edition** (formerly Team Edition) does not include API access. If you're on this tier, CSV export is your only option.

Bullhorn's authentication is a two-step process: OAuth to obtain credentials, then a separate `BhRestToken` session token for REST API calls. Your extraction scripts must handle both, including 307 redirects from the `loginInfo` endpoint. The REST API uses Lucene query syntax for `/search` operations and JPQL-like syntax for `/query` operations, with pagination via `start` and `count` parameters. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/index.html))

## Ceipal API Constraints

Ceipal's ATS API exposes endpoints for core entities: Applicants, Clients, Job Postings, Submissions, Placements, Leads, Vendors, Interviews, and Users. It also provides Master Data endpoints for picklist values (Job Types, Applicant Status, Industries, etc.). ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/applicants))

| Constraint | Detail |
|---|---|
| **Auth token lifetime** | 1 hour |
| **Refresh token lifetime** | 7 days |
| **Resume token lifetime** | 30 minutes |
| **Rate limiting** | Per user auth token; HTTP 429 on exceed |
| **List endpoint page size** | Commonly capped at 50 records |
| **Bulk import API** | No documented bulk/batch API |

The lack of a documented bulk import API means you'll be creating records one at a time or in small batches. This is the single biggest bottleneck on the Ceipal side. Plan for it.

If you're using Ceipal's v2 API, note that endpoints require `/v2/` with a trailing slash, and parameters have moved from `snake_case` to `camelCase`. The `searchKey` parameter on Job Postings supports AND terms, exact phrases in quotes, and parentheses grouping. ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/v1-to-v2-migration))

Ceipal also offers an "Apply Without Registration" endpoint for creating applicant records — useful for candidate imports but limited in field coverage.

> [!WARNING]
> Keep a permanent ID crosswalk. Bullhorn uses standard integer IDs; Ceipal v2 detail flows use encrypted IDs. If you lose the mapping between them, you cannot rebuild relationships after the fact. ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/v1-to-v2-migration))

## Migration Approaches

There are five ways to move data from Bullhorn to Ceipal. Each has a different cost, control, and complexity profile.

### Native CSV Export/Import

**How it works:** Export entity data from Bullhorn as CSV files (via reports or admin data export), transform the CSVs to match Ceipal's import templates, then upload via Ceipal's import wizard.

**When to use it:** Small agencies with fewer than 5,000 candidate records, minimal custom fields, and no need to preserve stage history or attachments.

| Pros | Cons |
|---|---|
| No engineering required | Flattens all relationships |
| Free | Loses Application V2 stage history |
| Fast for small datasets | No attachment migration |
| | No notes/activity history |
| | Manual field mapping, duplicate risk on re-import |

> [!WARNING]
> CSV exports from Bullhorn flatten the `ClientCorporation → ClientContact → JobOrder → JobSubmission → Placement` chain into disconnected rows. You'll spend more time rebuilding relationships manually than you saved by avoiding the API.

For a deeper look at why flat files fail for complex schemas, see [Using CSVs for SaaS Data Migrations: Pros and Cons](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

### API-Based Custom ETL Pipeline

**How it works:** Your engineering team builds extraction scripts against Bullhorn's REST API, transforms data in a staging layer (database or data warehouse), and loads into Ceipal via their API.

**When to use it:** Mid-size to large agencies with engineering bandwidth, complex data models, and a need to preserve relationships and history.

| Pros | Cons |
|---|---|
| Full control over mapping | Significant engineering investment |
| Preserves relationships | Must handle rate limits on both sides |
| Can migrate attachments | Bullhorn's 100K monthly call limit constrains speed |
| Handles custom fields | Ceipal lacks bulk API — slow writes |
| Scriptable and repeatable | Needs monitoring, logging, retry logic |

### Ceipal's Free Onboarding Migration

**How it works:** Ceipal offers complimentary data migration assistance for new customers. Their onboarding team handles the data transfer using standard mapping templates.

**When to use it:** Standard Bullhorn configurations with common field structures and moderate data volumes.

| Pros | Cons |
|---|---|
| Free — included with subscription | Limited control over mapping logic |
| Ceipal handles execution | Standard templates may miss custom objects |
| AI-powered data quality checks | Timelines can stretch if data is complex |
| | Highly customized Bullhorn environments may not fit |

Ceipal states their migration team has handled databases of up to 1 million records from over 30 ATS/CRM platforms. But this service is designed for standard configurations — if your Bullhorn instance is heavily customized with custom objects, complex stage configurations, or non-standard integrations, expect gaps. Confirm scope early. ([ceipal.com](https://www.ceipal.com/staffing-firm-solutions/data-migration))

### Middleware/iPaaS (Zapier, Make)

**How it works:** Use pre-built connectors to map fields between the two systems via polling or trigger-based workflows.

**When to use it:** Lightweight ongoing sync after cutover, not full historical migration.

| Pros | Cons |
|---|---|
| Visual builder, fast to stand up | Cannot handle bulk historical extraction |
| Low code | Limited object coverage |
| | Zapier's Ceipal app is polling-based (15-minute checks on the free plan) |
| | High volume triggers API overages and timeouts |

Middleware is useful for narrow, ongoing workflows after the main cutover — but it's a poor fit for migrating historical data at scale. ([zapier.com](https://zapier.com/apps/ceipal/integrations))

### Managed Migration Service

**How it works:** A dedicated migration service builds custom extraction and loading scripts for your specific Bullhorn-to-Ceipal scenario, handles all API orchestration, and validates data integrity.

**When to use it:** Enterprise datasets (100K+ records), complex relational data, custom objects, attachments, and when your team can't pull engineers off product work for weeks.

| Pros | Cons |
|---|---|
| Custom mapping for your exact schema | External cost |
| Handles rate limits, retries, token refresh | Requires vendor vetting |
| Preserves relationships and attachments | |
| Zero downtime during migration | |
| Validation and rollback built in | |

### Migration Approach Comparison

| Approach | Best For | Relationships Preserved | Attachments | Cost | Complexity |
|---|---|---|---|---|---|
| CSV Export/Import | <5K records, simple data | ❌ | ❌ | Free | Low |
| Custom ETL Pipeline | Teams with API experience | ✅ | ✅ | Engineering time | High |
| Ceipal Onboarding | Standard Bullhorn setups | Partial | Depends | Free | Low |
| Middleware/iPaaS | Lightweight ongoing sync | ❌ | ❌ | Subscription | Medium |
| Managed Service | Enterprise, complex data | ✅ | ✅ | Service fee | Low (your side) |

### Recommendations by Scenario

- **Small agency, clean data:** CSV or Ceipal onboarding can work if history requirements are low.
- **Enterprise:** API/ETL or a specialist managed migration service.
- **One-time migration:** API-led or managed service is usually the safest middle ground.
- **Ongoing sync:** Engineered APIs for anything relationship-heavy; iPaaS only for narrow workflows.
- **Low engineering bandwidth:** Do not DIY unless your team is ready to own retries, cutover, and UAT support.

## Object Mapping: Bullhorn → Ceipal

This is the core of the migration. Get this wrong and everything downstream breaks.

### Entity-Level Mapping

| Bullhorn Entity | Ceipal Equivalent | Notes |
|---|---|---|
| `Candidate` | `Applicant` | Direct mapping; field-level differences |
| `ClientCorporation` | `Client` | Company records |
| `ClientContact` | Client Contact (within Client) | Ceipal nests contacts under clients |
| `JobOrder` | `Job Posting` | Field names differ significantly |
| `JobSubmission` / `Application V2` | `Submission` | Most complex mapping. Bullhorn stage histories must translate to Ceipal pipeline stages. |
| `Placement` | `Placement` | Billing and financial fields differ |
| `Opportunity` | `Lead` or external CRM | Ceipal's public ATS docs do not show a first-class Opportunity resource. Redesign rather than 1:1 load. ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/applicant-details-copy)) |
| `Note` | Notes / Activity | No standalone "create note" endpoint in Ceipal v1 API |
| `Appointment` (type=Interview) | `Interview` | Watch for Bullhorn parent/child appointment duplication |
| `Application History` | No equivalent | Audit trail not portable |
| `Tearsheet` | No equivalent | Candidate lists — recreate manually |
| Custom Objects | Custom Fields | Flatten into Ceipal custom fields |

### Field-Level Mapping: Candidate → Applicant

| Bullhorn Field | Type | Ceipal Field | Type | Transform |
|---|---|---|---|---|
| `id` | Integer | (store as reference) | — | Map table only |
| `firstName` | String | `firstName` | String | Direct |
| `lastName` | String | `lastName` | String | Direct |
| `email` | String | `email` | String | Direct |
| `phone` | String | `phone` | String | Direct |
| `address.address1` | String | `address` | String | Flatten nested object |
| `address.city` | String | `city` | String | Direct |
| `address.state` | String | `state` | String | Direct |
| `address.zip` | String | `zip` | String | Direct |
| `status` | String | `applicantStatus` | Picklist | Map to Ceipal picklist values |
| `source` | String | `applicantSource` | Picklist | Map to Ceipal master data |
| `skillSet` | String | Skills | String/Tags | Parse comma-separated skills |
| `dateAdded` | Timestamp | `dateCreated` | Datetime | Unix ms → ISO 8601 |
| `customText1`–`customText20` | String | Custom Fields | String | Map by business meaning |
| `owner.id` | Integer | `recruiterId` | Integer | Map via user lookup table |

### Field-Level Mapping: JobOrder → Job Posting

| Bullhorn Field | Type | Ceipal Field | Type | Transform |
|---|---|---|---|---|
| `id` | Integer | (store as reference) | — | Map table only |
| `title` | String | `jobTitle` | String | Direct |
| `clientCorporation.id` | Integer | `clientId` | Integer | Lookup from mapping table |
| `clientContact.id` | Integer | Client Contact ref | Integer | Lookup from mapping table |
| `employmentType` | String | `employmentType` | Picklist | Map values |
| `status` | String | `jobStatus` | Picklist | Map to Ceipal's active/inactive/filled states |
| `payRate` | BigDecimal | `payRate` | Decimal | Direct |
| `clientBillRate` | BigDecimal | `billRate` | Decimal | Direct |
| `startDate` | Timestamp | `startDate` | Datetime | Unix ms → ISO 8601 |
| `description` | String | `jobDescription` | String | Direct (HTML may need cleanup) |
| `address` | Object | Location fields | Strings | Flatten nested address |

For more on handling custom data structures during ATS migrations, see [5 "Gotchas" in ATS Migration](https://clonepartner.com/blog/blog/ats-migration-gotchas/).

## Migration Architecture

Design the pipeline as **extract → stage → transform → load → validate → delta replay → cutover**. Keep the phases decoupled so you can rerun any step independently.

### Extraction (Bullhorn Side)

Use Bullhorn's `/search` endpoint for fast, indexed field queries and `/query` for relational data with JPQL-like filters. Store raw JSON payloads in an intermediate staging database (PostgreSQL, MongoDB, or structured JSON files).

Key implementation details:

- Authenticate via OAuth, then obtain a `BhRestToken` for REST calls. Handle 307 redirects from `loginInfo`. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/index.html))
- Query with `where=isDeleted=false` where supported
- Request fields explicitly — `fields=*` works but returns more data than needed
- Track `dateLastModified` for delta extractions
- Fetch files through `fileAttachments` and `/file` endpoints

```python
import requests
import time

BH_REST_URL = "https://rest{swimlane}.bullhornstaffing.com/rest-services/{corpToken}"
BH_TOKEN = "your-bh-rest-token"

def fetch_candidates(start=0, count=200):
    """Paginate through all Candidate records."""
    url = f"{BH_REST_URL}/query/Candidate"
    params = {
        "where": "isDeleted=false",
        "fields": "id,firstName,lastName,email,phone,address,status,dateAdded,customText1",
        "count": count,
        "start": start,
        "BhRestToken": BH_TOKEN
    }
    resp = requests.get(url, params=params)
    if resp.status_code == 429:
        time.sleep(1)
        return fetch_candidates(start, count)
    resp.raise_for_status()
    return resp.json()

def extract_all_candidates():
    """Extract all candidates with pagination."""
    all_candidates = []
    start = 0
    while True:
        batch = fetch_candidates(start=start)
        data = batch.get("data", [])
        if not data:
            break
        all_candidates.extend(data)
        start += len(data)
        if start >= batch.get("total", 0):
            break
    return all_candidates
```

### Transformation (Staging Layer)

Run scripts on the staging database to:

- Convert Bullhorn timestamps (Unix milliseconds) to Ceipal's datetime format (ISO 8601)
- Map picklist values using lookup tables (Bullhorn status → Ceipal status)
- Flatten nested address objects
- Parse skill sets into Ceipal's expected format
- Deduplicate records (email-based for candidates, name+address for companies)
- Pre-create any missing Ceipal picklist values via Master Data endpoints before loading records that reference them

Transform in the staging layer, not inline during load. This lets you inspect and re-transform without re-extracting from Bullhorn.

### Loading (Ceipal Side)

Push transformed data into Ceipal in strict dependency order. The 1-hour token expiry must be handled automatically — your script should detect both 401 responses and proactively refresh before expiry.

```javascript
const axios = require('axios');

class CeipalLoader {
  constructor(clientId, clientSecret, refreshToken) {
    this.apiBase = 'https://api.ceipal.com/v1';
    this.refreshToken = refreshToken;
    this.accessToken = null;
    this.tokenExpiry = 0;
  }

  async refreshAuthToken() {
    try {
      const response = await axios.post(`${this.apiBase}/oauth/access_token`, {
        grant_type: 'refresh_token',
        refresh_token: this.refreshToken
      });
      this.accessToken = response.data.access_token;
      // Set expiry to 55 minutes to be safe (Ceipal tokens last 60 mins)
      this.tokenExpiry = Date.now() + (55 * 60 * 1000);
    } catch (error) {
      console.error('Failed to refresh Ceipal token', error);
      throw error;
    }
  }

  async loadRecord(endpoint, payload) {
    if (Date.now() > this.tokenExpiry) {
      await this.refreshAuthToken();
    }

    try {
      const response = await axios.post(`${this.apiBase}/${endpoint}`, payload, {
        headers: { Authorization: `Bearer ${this.accessToken}` }
      });
      return response.data;
    } catch (error) {
      if (error.response && error.response.status === 429) {
        console.warn('Rate limit hit. Backing off...');
        // Implement exponential backoff with jitter here
      }
      throw error;
    }
  }
}
```

> [!TIP]
> Always store the Bullhorn source ID alongside every Ceipal record you create. You'll need this mapping table to rebuild relationships (e.g., linking Submissions to the correct Applicant and Job Posting) and to validate the migration.

## Handling Attachments, Notes, and Complex Relationships

These are the hardest parts of any ATS migration — and where most pipelines fail.

### Attachments (Resumes, Documents)

Bullhorn stores file attachments (resumes, offer letters, compliance documents) linked to Candidate, JobOrder, Placement, and other entities via the `/file` API endpoint. Each attachment has metadata (filename, type, content type) and binary content.

To migrate these, your script must:

1. Query the `FileAttachment` metadata to get the file ID
2. Make a separate API call to download the file content (often base64 encoded)
3. Decode the file and upload it to Ceipal's document endpoints using `multipart/form-data`

> [!CAUTION]
> Attachment migration is often the longest-running part of the pipeline. A database with 500K candidates and 2–3 files each means 1M+ API calls just for file downloads — that's several days at Bullhorn's 1,500 req/min limit. Run attachment extraction in a dedicated, heavily rate-limited background queue separate from your primary data extraction.

On the Ceipal side, resume downloads use a `resumeToken` that expires after 30 minutes. Factor this into any validation scripts that need to re-download and verify migrated files. ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/v1-to-v2-migration))

Large resume files (5MB+ PDFs) can cause API timeouts on either side. Build per-file retry logic with exponential backoff.

### Notes and Activities

Bullhorn Notes are linked to entities via `personReference`, `jobOrder`, or `clientCorporation` fields. A single note can reference multiple entities. The Ceipal API does not expose a standalone "create note" endpoint in its v1 API documentation. This means notes may need to be:

- Concatenated into a text field on the Applicant/Client record
- Imported via Ceipal's support/onboarding team
- Accepted as a data loss item

Be explicit with stakeholders about this gap before migration begins.

### Rebuilding Relationships

The migration must follow a strict loading order:

1. **Users** — map Bullhorn CorporateUser IDs to Ceipal User IDs
2. **Clients** (ClientCorporation → Client)
3. **Client Contacts** (ClientContact → nested under Client)
4. **Applicants** (Candidate → Applicant)
5. **Job Postings** (JobOrder → Job Posting, linking to Client)
6. **Submissions** (JobSubmission → Submission, linking Applicant + Job Posting)
7. **Placements** (Placement → Placement, linking Applicant + Job Posting + Client)

Every step depends on the ID mapping table from the previous step. Skip one, and all downstream relationships break.

## Pre-Migration Planning

Before writing a single line of migration code:

- [ ] **Inventory all Bullhorn entities** — Candidates, ClientCorporations, ClientContacts, JobOrders, JobSubmissions, Placements, Opportunities, Notes, Appointments, custom objects
- [ ] **Count records per entity** — this determines timeline and API budget
- [ ] **Confirm your Bullhorn edition** — ATS Growth edition does not include API access
- [ ] **Confirm ATS v1 vs. ATS v2** — the data model differs significantly
- [ ] **Verify permissions** — ensure the migration user has access to confidential fields and file attachments. Bullhorn can return `CONFIDENTIAL` for restricted candidate fields, giving you clean record counts but corrupted field data. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/entityref.html))
- [ ] **Identify unused data** — archived candidates with no activity in 3+ years, closed jobs, deleted contacts
- [ ] **Document custom fields** — map every `customText`, `customDate`, `customFloat`, and custom object to its business meaning
- [ ] **Audit picklist values** — Bullhorn status values rarely match Ceipal's master data 1:1
- [ ] **Catalog attachments** — count files, total size, types
- [ ] **Define migration scope** — what moves, what stays, what gets archived
- [ ] **Choose cutover strategy:**
  - **Big bang** — everything at once, cutover over a weekend
  - **Phased** — move by entity type or business unit
  - **Incremental** — ongoing sync during transition period
- [ ] **Set up a Ceipal sandbox** — run test migrations before touching production
- [ ] **Build a rollback plan** — full Bullhorn backup, documented restore procedure, defined rollback criteria

Incremental cutovers are feasible because Ceipal list endpoints expose `modifiedAfter`/`modifiedBefore` filters, while Bullhorn supports `dateLastModified` fields for delta queries. ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/applicants))

Lock all Bullhorn custom field creation at least two weeks before migration to prevent schema drift.

## Common Failure Modes

**Duplicate records.** Bullhorn allows a Candidate to also be a ClientContact (and vice versa). If your migration doesn't account for this, you'll create duplicate people in Ceipal.

**Picklist mismatches.** Bullhorn's `status` field on a Candidate might have values like "Placed", "Available", "Do Not Contact". Ceipal's `applicantStatus` uses a different set. Unmapped values silently default or fail.

**Missing Application History.** Ceipal has no equivalent to Bullhorn's Application V2 audit trail. Recruiters who rely on stage history for compliance reporting will lose that visibility.

**Custom object data loss.** Bullhorn supports up to 35 custom objects per entity. Ceipal's custom field capacity is more limited. If you can't map every custom object field, data is lost.

**Confidential field masking.** Bullhorn candidate fields can return `CONFIDENTIAL` if the migration user lacks the necessary entitlements. You'll get correct record counts but corrupted field data — one of the harder problems to catch because the API doesn't error. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/entityref.html))

**Duplicate interviews.** Bullhorn creates parent/child Appointment records per invitee. If extracted naively, a single interview produces multiple records in Ceipal. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/entityref.html))

**Attachment timeouts.** Large resume files (5MB+ PDFs) can cause API timeouts on either side. Build per-file retry logic with exponential backoff.

**Token expiry mid-batch.** Ceipal's 1-hour auth token can expire during a long import run. Your script must detect 401 responses and refresh the token automatically.

## Limitations You Must Accept

Some things don't translate between these two platforms. It's better to know upfront than discover mid-migration:

- **Application V2 / Application History** — no Ceipal equivalent. Stage history is lost or flattened to current status only.
- **Tearsheets** (Bullhorn's hot lists) — no API-accessible equivalent in Ceipal. Recreate manually.
- **Custom Objects** — Bullhorn's extensible custom object model doesn't map to Ceipal. Flatten into custom fields or accept data loss.
- **Opportunity** — Ceipal's public ATS docs do not expose a first-class Opportunity resource. This data needs redesign or external retention. ([developer.ceipal.com](https://developer.ceipal.com/ceipal-ats-v2/applicant-details-copy))
- **Bullhorn's Lucene search indexes** — Ceipal has its own search infrastructure. Saved searches don't migrate.
- **Email integration history** — Bullhorn tracks email correspondence; this history typically won't transfer.
- **Bullhorn-specific automations and workflows** — must be rebuilt natively in Ceipal.

Predictable data-loss scenarios: stage history collapsed to current status only, duplicate interviews from child appointments, missing attachments, masked confidential fields, and custom-object data pushed into unsearchable text fields.

## Validation and Testing

Never execute a production load without running a full sandbox test first.

### Record Count Comparison

After migration, run a count for every entity type:

| Entity | Bullhorn Count | Ceipal Count | Delta |
|---|---|---|---|
| Candidates / Applicants | X | Y | Y - X |
| Companies / Clients | X | Y | Y - X |
| Jobs / Job Postings | X | Y | Y - X |
| Submissions | X | Y | Y - X |
| Placements | X | Y | Y - X |

Any non-zero delta needs investigation.

### Field-Level Sampling

Pull 50 random records from each entity type. Compare every mapped field between source and target. Flag:

- Truncated values (field length limits)
- Encoding issues (special characters, UTF-8)
- Null values where data should exist
- Incorrect picklist mappings
- Epoch timestamps that didn't convert correctly

### Relationship Integrity

Pick 50 placements and confirm they link to the correct candidate, job, and client in Ceipal. Verify that every Submission points to an existing Applicant and Job Posting. Broken FK references are the most common silent failure.

### UAT with Recruiters

Give 3–5 recruiters a list of 20 known candidates each. Have them verify:

- Contact information is correct
- Placement history is intact
- Notes are present (if migrated)
- Resumes are attached (if migrated)
- Job assignments are correctly linked

Run UAT on real recruiter workflows, not just exported rows.

### Rollback Plan

Before cutover:

- Confirm full Bullhorn data backup exists
- Document the restore procedure
- Define rollback criteria (e.g., >5% record count mismatch triggers rollback)
- Keep Bullhorn active for 30 days post-migration as a safety net

## Post-Migration Tasks

Once the data is live in Ceipal:

- **Rebuild automations.** Bullhorn workflows, triggers, and saved searches don't transfer. Recreate them in Ceipal's workflow builder.
- **Configure integrations.** Reconnect job boards, email, calendar, and VMS integrations in Ceipal.
- **Train recruiters.** Ceipal's UI and terminology differ from Bullhorn. Plan 1–2 days of hands-on training.
- **Monitor for 30 days.** Watch for missing data, broken links, and recruiter-reported issues. Run daily count comparisons for the first week.
- **Monitor API error logs.** For the first 48 hours, ensure no lingering integrations are trying to push data to the old Bullhorn instance.
- **Decommission Bullhorn.** Only after validation is complete and the team has been running on Ceipal for at least 30 days.

## Best Practices

1. **Extract everything, load selectively.** Pull your entire Bullhorn database into your staging environment. Even if you don't plan to load data older than 5 years into Ceipal, having the raw JSON backup is invaluable.
2. **Run at least two test migrations** in a Ceipal sandbox before touching production.
3. **Build an ID mapping table** (`bullhorn_id` → `ceipal_id`) for every entity. This is your migration's source of truth.
4. **Validate incrementally** — don't wait until the end to check data.
5. **Automate picklist mapping** — build a lookup table, not a switch statement. Pre-create Ceipal master data values before loading.
6. **Log every API call and response** — you'll need this for debugging and audit.
7. **Lock Bullhorn configurations.** Freeze all custom field creation at least two weeks before migration to prevent schema drift.
8. **Plan for the delta.** A migration of 500GB of data might take three days to process. Run the bulk migration, let your team keep working in Bullhorn, then run a delta sync over the weekend to catch records modified during the bulk run.
9. **Plan for twice the timeline you estimate.** Attachments and edge cases always take longer than expected.

## When to Use a Managed Migration Service

Build in-house when:

- You have dedicated engineers with API integration experience
- Your Bullhorn instance is relatively standard
- You're migrating fewer than 50,000 total records
- You can afford 2–4 weeks of engineering time

Use a managed service when:

- Your engineering team is focused on product work and can't spare the bandwidth
- You have 100K+ records with complex relationships and attachments
- Your Bullhorn instance uses extensive custom objects
- You need zero downtime — recruiters must keep working during migration
- You don't want to build retry logic, token management, and rate-limit handling for a one-time migration

The code you write to handle Bullhorn's pagination, session limits, and 429 errors is throwaway code — it has zero business value once the migration is complete. The hidden costs go beyond engineering hours: debugging time when records don't match, recruiter downtime when data is partially loaded, and the risk of silent data corruption that surfaces weeks later.

For a deeper analysis of why migration should be separated from implementation work, see [Why Data Migration Isn't Implementation](https://clonepartner.com/blog/blog/data-migration-vs-implementation-guide/).

### Why Engineering Teams Choose ClonePartner

ClonePartner treats data migration as an engineering discipline, not a side project. For Bullhorn-to-Ceipal specifically:

- **Custom scripts handle Bullhorn's 429 rate limits, pagination, and session management** — your team doesn't build retry logic for a one-time move.
- **Deep experience with relational ATS data models** — we've mapped Bullhorn's Application V2, JobSubmission, and Placement hierarchies into multiple target systems.
- **Attachment and note migration included** — we don't skip the hard parts.
- **Zero downtime guarantee** — your recruiters keep working in Bullhorn until the exact cutover moment. We run the bulk migration, then execute a delta sync to catch any records modified during the process.
- **Full validation** — record counts, field-level sampling, and relationship integrity checks before handoff.

We handle the migration so your engineers stay on product work. That's the value proposition — not a tool, but an outcome.

> Migrating from Bullhorn to Ceipal? Talk to our team about your data model, timeline, and volume. We'll tell you exactly what's involved — no sales pitch, just a technical scoping call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## Frequently asked questions

### Can I migrate from Bullhorn to Ceipal using CSV exports?

Yes, but CSV exports flatten relationships and lose Application V2 stage history, attachments, and notes. It's only viable for small datasets under 5,000 records with simple field structures.

### What are Bullhorn's API rate limits for data migration?

Bullhorn allows 1,500 API requests per minute per OAuth Client ID, up to 50 concurrent sessions (Enterprise), and 100,000 total API calls per month unless negotiated. Exceeding the per-minute limit returns a 429 error — wait 1 second and retry. Calls returning 429 do not count against your limits.

### Does Ceipal offer free data migration from Bullhorn?

Yes, Ceipal provides complimentary data migration assistance for new customers. Their team has migrated databases of up to 1 million records from 30+ ATS platforms. However, highly customized Bullhorn environments with custom objects or complex stage configurations may exceed their standard mapping templates.

### What Bullhorn data cannot be migrated to Ceipal?

Application V2 stage history, Application History audit trails, Tearsheets (hot lists), Opportunities (no first-class Ceipal equivalent), saved Lucene searches, email integration history, and Bullhorn-specific workflow automations have no direct Ceipal equivalents. Custom objects must be flattened into custom fields.

### How long does a Bullhorn to Ceipal migration take?

Timelines vary by volume and complexity. A small agency (<5K records) with CSV import can finish in days. Enterprise migrations (100K+ records) with attachments and custom objects typically take 2–6 weeks including testing and validation.
