Skip to content

JazzHR to Greenhouse Migration: The CTO's Technical Guide

A CTO-level guide to JazzHR to Greenhouse migration — covering API limits, data model mapping, attachment handling, GDPR traps, and every viable method.

Raaj Raaj · · 20 min read
JazzHR to Greenhouse 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,200+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Migrating from JazzHR to Greenhouse is a schema-expansion and relationship-mapping problem, not a CSV copy-paste. JazzHR is an SMB-focused ATS built around a flat applicant-job model with lightweight workflows. Greenhouse is an enterprise-grade structured hiring platform that separates Candidates from Applications, enforces scorecard-driven evaluation pipelines, and maintains strict stage hierarchies per job. Moving data between these systems means translating a simple applicant record into a multi-object graph — Candidate → Application → Job → Stage — while working around API constraints on both sides.

The fundamental bottlenecks: JazzHR's API is read-heavy with no bulk export endpoint and a locked-down file extraction endpoint. Greenhouse's bulk import caps at 8,000 rows per upload and supports only four milestone values, compressing detailed stage history. Greenhouse will also silently fire GDPR/CCPA compliance emails to imported historical candidates unless you preemptively disable consent rules. Between these constraints sits every architectural decision this guide covers.

If you're evaluating other ATS migrations or want broader context, see our guides on common ATS migration gotchas, GDPR/CCPA compliance during candidate data transfers, and when CSVs are the wrong migration tool.

Warning

Greenhouse Harvest API v1/v2 is deprecated and will be removed on August 31, 2026. New migration scripts should target Harvest v3 with OAuth authentication. Do not build new integrations against v1/v2 endpoints.

Warning

Do not migrate from a single flat CSV if one JazzHR candidate has multiple job applications. Preserve both immutable source keys end-to-end: the person key (applicant_id) and the application key (appjob_id). JazzHR's candidate export docs expose both, and they are what keep deduplication from destroying application history. (apidoc.jazzhrapis.com)

Why Companies Migrate from JazzHR to Greenhouse

The drivers are structural, not cosmetic:

  • Scaling past JazzHR's ceiling. JazzHR targets companies with fewer than 500 employees. Once hiring volume crosses ~50 open roles or multiple geographies, the lack of structured scorecards, approval workflows, and configurable interview plans becomes a bottleneck.
  • Structured hiring enforcement. Greenhouse enforces consistent, auditable hiring — scorecards tied to interview stages, required feedback before advancing, and attribute-based evaluation. JazzHR's workflow system is lighter and more freeform.
  • Integration ecosystem. Greenhouse has five distinct APIs (Harvest, Ingestion, Job Board, Assessment, Onboarding) and a deep integration catalog. JazzHR's integration surface is narrower — its API supports only GET and POST methods with no PUT or DELETE.
  • Reporting depth. Greenhouse provides pipeline velocity, pass-through rates, and source effectiveness at a granularity JazzHR's reporting cannot match.

If your real priority is a lighter-weight ATS with less admin surface area, JazzHR may still be the better fit. Not every company needs Greenhouse's level of process enforcement.

Data Model Differences: Mapping JazzHR to Greenhouse

This is where most migrations go wrong. JazzHR uses a flat, applicant-centric model. Greenhouse uses a normalized, application-centric model with distinct entity types. If you do not map this relationship correctly, you end up with duplicate candidates or orphaned applications.

A useful framing: a candidate is the person record; an application is that person's candidacy for one specific job. In JazzHR's candidate export payload, a single candidate can contain multiple profiles, each representing a different job application. JazzHR documents both a person identifier (applicant_id) and a job-application identifier (appjob_id). Preserve both keys end-to-end or you will not be able to deduplicate people while keeping distinct candidacies. (apidoc.jazzhrapis.com)

Core Object Mapping

JazzHR Object Greenhouse Equivalent Notes
Applicant Candidate 1:1 mapping. Greenhouse separates the person (Candidate) from their candidacy (Application).
Job Job Greenhouse Jobs have structured interview plans with stages. JazzHR Jobs have simpler workflow steps.
Applicant2Job (join record) Application The link between a Candidate and a Job. Greenhouse Applications carry stage, source, and rejection data.
Workflow Step Job Stage JazzHR workflow steps map to Greenhouse interview plan stages, but stages in Greenhouse are per-job, not global.
Category Tag / Custom Field JazzHR categories are labels on applicants. Greenhouse uses tags and custom candidate/application fields.
Activity Activity Feed / Note JazzHR activities become notes or activity entries on the Greenhouse candidate profile.
Questionnaire Answers Custom Application Fields JazzHR screening questions map to Greenhouse custom application fields. Requires pre-creating the fields in Greenhouse.
Hire Record Hired Application JazzHR tracks hires as a separate object. In Greenhouse, "hired" is a status on an Application.
Resume / Cover Letter Attachment The hardest part of the migration. See the attachment section below.

The Container Job Strategy for Historical Data

You do not want to recreate every closed, historical job from JazzHR in Greenhouse. Greenhouse officially recommends a container job approach: create a single job named something like HISTORICAL IMPORT, mark it as a Template so it is excluded from active reporting, and bulk-import all past candidates into it. Active candidates should be imported into their actual destination jobs. (support.greenhouse.io)

Field-Level Mapping Reference

The table below reflects the public JazzHR candidate export identifiers and Greenhouse's candidate/application model. (apidoc.jazzhrapis.com)

JazzHR Field Greenhouse Target Notes
applicant_id Custom candidate field legacy_jazzhr_applicant_id Preserve for idempotency and deduplication
appjob_id Custom application field or external mapping table Required when one person applied to multiple jobs
first_name / last_name first_name / last_name Direct string mapping
email email_addresses [].value Array of objects in Greenhouse
prospect_phone phone_numbers [].value Array of objects; normalize format before load
apply_date applied_at (on Application) ISO-8601 datetime
rating Custom field or tag No native equivalent in Greenhouse
workflow_step_name Stage on Application Map to Greenhouse interview plan stage or bulk-import Milestone
city, state addresses [].value Concatenate into structured address
source source.public_name Must match or pre-create source in Greenhouse
Custom fields Custom Candidate/Application Fields Pre-create in Greenhouse; match by name; field-type transforms often required
Info

Greenhouse separates candidate-level custom fields from application-level custom fields. JazzHR custom fields tied to a candidate map to Greenhouse candidate fields; custom fields tied to a job application map to application fields. Mixing these up causes data to land in the wrong place. Application-level custom fields may be plan-dependent. (support.greenhouse.io)

Info

Greenhouse custom option sync works for many single- and multi-select custom fields, but not for metadata like Office, Department, Sources, Candidate Tags, or Rejection Reasons. Those must be pre-created manually. (support.greenhouse.io)

Migration Approaches: API vs. CSV vs. iPaaS

There are four viable methods. Each has real trade-offs depending on your data volume, engineering capacity, and tolerance for data loss.

Method 1: Native CSV Export + Greenhouse Bulk Import

How it works: Export candidate data from JazzHR using the Candidate Record Raw Data Download in Reports, format it to match Greenhouse's bulk import template, and upload via Greenhouse's Configure → Bulk Import tool. Greenhouse recommends separate imports for active candidates, historical rejected candidates, and historical hired candidates. (support.greenhouse.io)

When to use it: Under 5,000 total candidates, no complex custom field mapping, and you can accept milestone-level stage compression.

Pros:

  • No code required
  • Built into both platforms
  • Greenhouse supports ZIP resume upload in the same workflow

Cons:

  • Greenhouse bulk import is capped at 8,000 rows per upload — larger datasets require manual batching
  • JazzHR's CSV export flattens relationships — you lose applicant-to-job association granularity
  • The bulk import tool only supports four Milestone values: Application, Assessment, Face to Face, and Offer. Detailed stage history is compressed. (support.greenhouse.io)
  • Resumes require a separate ZIP file upload with filename-based matching, which is error-prone at scale
  • Historical candidates imported as hired or rejected automatically receive GDPR/CCPA compliance emails unless you disable consent extension email rules before importing
  • Bulk import is only available on Greenhouse Plus and Pro subscription tiers
  • Jobs must be open to appear in the mapping interface — candidates linked to closed or unmapped jobs are silently skipped

Complexity: Low | Risk: Medium (data loss on relationships, stages, and attachments)

For more on CSV-based migration trade-offs, see Using CSVs for SaaS Data Migrations: Pros and Cons.

Method 2: Custom API-Based ETL Pipeline

How it works: Extract data from JazzHR's REST API (api.resumatorapi.com/v1/), load raw data into a staging store, transform it, and load into Greenhouse via the Harvest API. This is the only approach that gives full control over object relationships, attachment linking, and stage preservation.

When to use it: Over 5,000 candidates, complex custom field mapping, need to preserve job-applicant relationships, or need attachment migration.

Pros:

  • Full control over data transformation and validation
  • Preserves all relationships (applicant → job → workflow step → application → stage)
  • Handles custom fields, tags, and notes programmatically
  • Can upload attachments via Greenhouse's POST /candidates/{id}/attachments endpoint
  • Clean path to ongoing delta sync through JazzHR candidate export webhooks
  • Best auditability and idempotency story

Cons:

  • JazzHR paginates at 100 results per page — extracting 50,000 applicants requires 500+ sequential API calls (resumatorapi.com)
  • JazzHR's API cannot extract files via the GET /files endpoint — it was locked down after the Offers and e-Signatures feature launched
  • Greenhouse Harvest API rate limit: 50 requests per 10 seconds (v1/v2) or a 30-second window (v3). Creating 10,000 candidates with attachments takes significant time with proper throttling (developers.greenhouse.io)
  • Greenhouse write operations may return truncated responses — Greenhouse recommends polling the GET endpoint every 30 seconds until the full record resolves
  • Requires the On-Behalf-Of header for all write operations (Greenhouse user ID for audit attribution)
  • Engineering effort: 2–4 weeks for a mid-level developer, including error handling and validation

Complexity: High | Risk: Low (if implemented with retry logic and validation)

Method 3: Middleware / iPaaS (Zapier, Make)

How it works: Use Zapier or Make to connect JazzHR triggers to Greenhouse actions. JazzHR's candidate export webhook integrates with iPaaS platforms, and Greenhouse's Zapier integration supports per-record triggers and actions.

When to use it: Low-volume ongoing sync after the main migration. Not suitable for bulk historical migration.

Pros:

  • No code for simple trigger-action flows
  • Good for bridging the gap during a phased migration

Cons:

  • Not designed for large historical backfills
  • Rate limits on both sides make large-scale moves impractical
  • Limited field mapping — custom fields often require custom code steps
  • Per-record error handling becomes painful at migration scale
  • Cost scales with task volume

Complexity: Low (for sync) | Risk: High for migration use cases

Method 4: Unified API Platforms (Merge.dev, Knit)

How it works: Unified API platforms abstract both JazzHR and Greenhouse behind a common data model. Merge.dev, for example, maps JazzHR's applicant, activity, and user endpoints to a normalized ATS schema. (merge.dev)

When to use it: Building a product that needs to read from multiple ATS platforms, or when you want faster delivery than a from-scratch connector but still need more control than CSV.

Pros:

  • Single integration covers both source and target
  • Handles auth and pagination abstraction
  • Shorter connector build time

Cons:

  • Designed for ongoing integration, not one-time migration
  • Abstraction leaks are real — coverage for custom artifacts varies by vendor
  • May not expose all fields (JazzHR questionnaire answers, for example)
  • Not cost-effective for a one-time data move
  • Teams often underestimate the last 20%: attachments, historical statuses, duplicate handling

Complexity: Medium | Risk: Medium (abstraction hides edge cases)

Comparison Table

Criteria CSV Export/Import Custom ETL (API) Zapier/Make Unified API
Best for Small, simple backfill Full-fidelity migration Ongoing sync Product integration
Max scale ~8K per batch Unlimited (with throttling) Low-medium Unlimited
Preserves relationships Partial Full Partial Partial
Attachment migration ZIP (filename matching) API-based No No
Stage fidelity Milestone only (4 values) Full Partial Normalized
Custom fields Column mapping Programmatic Limited Normalized
Engineering effort None 2–4 weeks Low Medium
One-time migration

Recommendation by Scenario

  • Small business, low engineering bandwidth: CSV only if history is simple and attachment needs are light. Otherwise use a managed service.
  • Enterprise or regulated environment: Custom ETL or managed service, not pure CSV.
  • One-time migration with a dedicated dev team: Build a staged ETL if you need repeatable dry runs and auditable reconciliation.
  • Ongoing sync after cutover: Webhook-driven middleware or a unified API layer. Do not confuse this with historical migration.

API Constraints, Rate Limits, and Export Bottlenecks

These are the technical limits that shape your migration architecture.

JazzHR API Constraints

  • Pagination: 100 results per page. Append /page/# to the request URI. No cursor-based pagination — page numbers only. (resumatorapi.com)
  • Authentication: API key passed as query parameter (?apikey=YOUR_KEY). Key found in Settings → Integrations.
  • Methods: GET and POST only. No PUT or DELETE. You cannot modify or remove records via API.
  • File extraction: The GET /files endpoint is locked down. You cannot programmatically extract resumes, offer letters, or other documents via API.
  • Candidate Export Webhook: JazzHR offers a webhook-based export that sends candidate data (including documents as base64) to a configured URL. Documents are available via temporary public URLs valid for 2 hours and 30 minutes. This is a per-candidate, event-driven mechanism — not a bulk export. (apidoc.jazzhrapis.com)
  • Bulk export: No API-based bulk export. The Candidate Record Raw Data Download in Reports generates a CSV. For a full data export including attachments, contact JazzHR support — this process averages 15–20 business days.
Danger

The JazzHR API cannot extract files. If you need resumes and cover letters migrated, you must use JazzHR's in-app bulk resume download, request a full data export from JazzHR support (15–20 business days), or use the Candidate Export Webhook on a per-candidate basis.

Greenhouse Harvest API Constraints

  • Rate limit: 50 requests per 10 seconds (v1/v2). Harvest v3 uses a 30-second window. Exceeding returns HTTP 429 with Retry-After and X-RateLimit-Reset headers. (developers.greenhouse.io)
  • Pagination: Link-header based (RFC 5988). Max per_page value is 500. Harvest v3 only returns a next link — no prev or last, so you cannot calculate total pages upfront.
  • Authentication: HTTP Basic Auth (API token as username, blank password, Base64-encoded). Harvest v3 uses OAuth.
  • Write operations: Require On-Behalf-Of header with a Greenhouse user ID. All writes are attributed to this user for audit purposes.
  • Async record creation: POST requests for candidates and applications may return truncated responses. The full record is not immediately available — poll the GET endpoint every 30 seconds until the record resolves.
  • Attachments: Uploaded as base64-encoded content or via URL. Hosted on AWS S3 with temporary signed URLs — do not cache these for future use.
  • Office/department hierarchy: The Harvest API can add and edit offices and departments, but cannot delete them or change the hierarchy. Configure this manually in the Greenhouse UI before migration. (support.greenhouse.io)

Critical Edge Cases and Failure Modes

ATS migrations fail in the edge cases, not the happy path. These are documented behaviors that catch most teams off guard.

The GDPR/CCPA Auto-Email Trap

When you import historical candidates into Greenhouse with a status of "hired" or "rejected," Greenhouse automatically sends GDPR/CCPA consent/information emails to those candidates — unless you explicitly disable the consent extension email rules before the import. (support.greenhouse.io)

Importing 15,000 historical candidates and having all of them receive an unexpected email from a company they interviewed with three years ago is not hypothetical — it is a documented Greenhouse behavior and a serious brand risk.

Mitigation: Before any import, go to Greenhouse Settings → Data Privacy → Consent Rules and disable automated consent emails. Re-enable after import and validation.

For a full treatment of compliance risks, see Ensuring GDPR & CCPA Compliance When Migrating Candidate Data.

The Attachment Problem

This is the single hardest part of a JazzHR-to-Greenhouse migration:

  1. JazzHR's API will not give you files. The GET /files endpoint is locked.
  2. JazzHR's in-app bulk resume download gives you a ZIP, but filenames may not map cleanly to candidate records.
  3. JazzHR's Candidate Export Webhook provides documents as base64, but it is a per-candidate, event-driven export — not a bulk mechanism. Document URLs expire after 2 hours and 30 minutes. (apidoc.jazzhrapis.com)
  4. A full data export from JazzHR support (15–20 business days) returns resumes as a separate ZIP file.

Once you have the files, Greenhouse accepts attachments via:

  • Bulk import: A single ZIP file uploaded during the spreadsheet import, matched by filename. Matching depends on parseable files and email matching — error-prone at scale. (support.greenhouse.io)
  • Harvest API: POST /candidates/{id}/attachments with base64-encoded content or a URL. More reliable but adds per-candidate API calls against the rate limit.

Budget for the attachment problem explicitly. It is the #1 time sink in JazzHR migrations.

Duplicate Candidates

If auto-merge is enabled in Greenhouse, candidates imported via bulk import are evaluated against existing records using configured match criteria. Candidates sourced as referrals or from specific agencies will not auto-merge — they create duplicates with a potential duplicate alert. Critically, merged candidate profiles cannot be un-merged. (support.greenhouse.io)

Mitigation: Disable auto-merge during historical rejected/hired imports. Re-enable only after validation.

Missing Job Mappings

Every job in a Greenhouse bulk import spreadsheet must be mapped to an existing, open Greenhouse job. Candidates linked to unmapped or closed jobs are silently skipped. This is a common source of "missing" candidate records after migration.

Reporting Drift

Greenhouse excludes migrated candidates from essential reports by default. Unless you explicitly include them using the migrated-candidates filter, your pipeline and hiring reports will undercount. (support.greenhouse.io)

Questionnaire Data Loss

JazzHR questionnaire answers are structured as separate objects with questionnaire_id, applicant_id, job_id, and answer values. Greenhouse has no native questionnaire concept — you must map each question to a custom application field and transform the data. If you skip this step, the screening data is lost.

Step-by-Step Migration Runbook

A safe migration architecture follows this pattern:

JazzHR API + candidate export webhooks
    → raw extract store
    → normalization / staging tables
    → ID map + field transforms
    → Greenhouse load workers
    → validation + reconciliation reports

Step 1: Pre-Migration Data Audit

  • Count records: Applicants, jobs, hires, activities, questionnaire answers, categories, custom fields.
  • Identify dead data: Closed jobs older than your retention policy, duplicate applicants, test records.
  • Scope the migration: Decide which historical data matters. Most teams migrate all-time candidates but only active jobs.
  • Inventory custom fields: List every JazzHR custom field (candidate-level and job-level) and decide its Greenhouse target.
  • Document sources and categories: These need to be pre-created in Greenhouse before import.
  • Drop dead weight early: Unused custom fields, broken attachments, orphaned users.

Step 2: Extract Data from JazzHR

Use the JazzHR API with paginated loops. The 100-record page limit means you need resumable pagination with checkpoints, not a one-shot export loop.

import requests
import time
 
JAZZHR_API_KEY = "your_api_key"
BASE_URL = "https://api.resumatorapi.com/v1"
 
def extract_all_applicants():
    applicants = []
    page = 1
    while True:
        url = f"{BASE_URL}/applicants/page/{page}?apikey={JAZZHR_API_KEY}"
        response = requests.get(url)
        if response.status_code != 200:
            break
        batch = response.json()
        if not batch:
            break
        applicants.extend(batch)
        page += 1
        time.sleep(0.5)  # Be conservative with request frequency
    return applicants
 
def extract_applicant_jobs():
    """Extract the join records linking applicants to jobs."""
    records = []
    page = 1
    while True:
        url = f"{BASE_URL}/applicants2jobs/page/{page}?apikey={JAZZHR_API_KEY}"
        response = requests.get(url)
        if response.status_code != 200:
            break
        batch = response.json()
        if not batch:
            break
        records.extend(batch)
        page += 1
        time.sleep(0.5)
    return records

Repeat for /jobs, /activities, /hires, /categories, /categories2applicants, and /notes. Store raw payloads — you will want immutable source data for validation and debugging.

For attachments, use the in-app bulk resume download or request a full export from JazzHR support.

Warning

If you use JazzHR's Candidate Export Webhook for documents, ingest the files immediately. Document URLs are public for only 2 hours and 30 minutes after export. Treat the webhook as a short-lived transfer event, not a permanent document store. (apidoc.jazzhrapis.com)

Step 3: Prepare Greenhouse Foundation

Before loading any candidate data:

  • Create all target entities: Jobs (or a container job for historical data), departments, offices, sources, tags, and custom fields. Only open jobs appear in bulk import mapping.
  • Configure office/department hierarchy in the UI. The Harvest API cannot delete or reparent these after creation.
  • Disable GDPR/CCPA consent emails. Go to Settings → Data Privacy → Consent Rules and disable automated consent extension emails. This prevents compliance emails to imported historical candidates.
  • Provision API credentials. Create Harvest API keys with only the endpoints you need. Resolve target job IDs, stage IDs, user IDs, and custom field IDs.

Step 4: Transform and Map Data

Key transformations:

  • Flatten JazzHR's applicant + applicant2job into Greenhouse's candidate + application model. One applicant with three job associations becomes one Candidate with three Applications.
  • Preserve legacy IDs. Store applicant_id as a custom candidate field and appjob_id as a custom application field (or in an external mapping table). These are essential for deduplication, idempotent reruns, and post-migration validation.
  • Map workflow step names to Greenhouse stages. Build a lookup table if Greenhouse stages differ from JazzHR workflow steps.
  • Convert field formats. JazzHR's flat fields become Greenhouse's array-based model — email becomes email_addresses: [{value: "...", type: "personal"}].
  • Handle nulls and type mismatches. JazzHR may return empty strings where Greenhouse expects null or omitted fields.
  • Pre-create all required picklist values. Sources, tags, and custom field options must exist in Greenhouse before being referenced.

Step 5: Load into Greenhouse

Use the Harvest API to create candidates and applications, then attach notes, documents, and set final statuses.

import requests
import base64
import time
 
GH_API_TOKEN = "your_greenhouse_api_token"
GH_USER_ID = "12345"  # Greenhouse user ID for On-Behalf-Of
 
def create_candidate(candidate_data):
    url = "https://harvest.greenhouse.io/v1/candidates"
    credentials = base64.b64encode(f"{GH_API_TOKEN}:".encode()).decode()
    headers = {
        "Authorization": f"Basic {credentials}",
        "Content-Type": "application/json",
        "On-Behalf-Of": GH_USER_ID
    }
    response = requests.post(url, json=candidate_data, headers=headers)
 
    if response.status_code == 429:
        wait = int(response.headers.get("Retry-After", 10))
        time.sleep(wait)
        return create_candidate(candidate_data)  # Retry
 
    response.raise_for_status()
    return response.json()
 
def add_attachment(candidate_id, filename, content_base64, content_type):
    url = f"https://harvest.greenhouse.io/v1/candidates/{candidate_id}/attachments"
    credentials = base64.b64encode(f"{GH_API_TOKEN}:".encode()).decode()
    headers = {
        "Authorization": f"Basic {credentials}",
        "Content-Type": "application/json",
        "On-Behalf-Of": GH_USER_ID
    }
    payload = {
        "filename": filename,
        "type": "resume",
        "content": content_base64,
        "content_type": content_type
    }
    return requests.post(url, json=payload, headers=headers)

The typical API sequence: create candidate → add application → add notes and attachments → advance, reject, or hire the application to match its final state. Greenhouse exposes explicit advance, reject, and hire endpoints — use that separation deliberately instead of trying to fake everything in one payload. (developers.greenhouse.io)

Tip

After creating a candidate via POST, the API may return a truncated response. Poll GET /candidates/{id} every 30 seconds until the full record is available. A 404 may mean the record is still being created.

Step 6: Handle Errors and Throttling

Build exponential backoff into your pipeline. Monitor the X-RateLimit-Remaining header proactively — do not wait for 429 errors. Greenhouse recommends slowing requests when the remaining count drops below 10.

if response.status_code == 429:
    time.sleep(int(response.headers.get("Retry-After", 10)))
    retry()
elif response.status_code >= 500:
    backoff_and_retry()
elif response.status_code == 422:
    dead_letter(response.json())  # Validation failure with field-level details

Keep the logic idempotent so reruns do not create duplicate state transitions. Dead-letter anything that fails validation for manual review.

Step 7: Validate

  • Record count comparison: Total applicants in JazzHR vs. total candidates in Greenhouse.
  • Relationship integrity: Verify that each applicant2job record in JazzHR has a corresponding Application in Greenhouse.
  • Field-level sampling: Pull 50–100 random candidates and compare every field against JazzHR source data.
  • Attachment verification: Confirm resumes are accessible on candidate profiles.
  • Stage accuracy: Check that candidates appear in the correct job stages.
  • Edge case sampling: Specifically check candidates with multiple applications, referral sources, and historical hires.
  • UAT with recruiters: Have recruiters validate 20–30 candidate profiles manually, checking stage placement, attachments, custom fields, and notes.
  • Reporting check: Include migrated candidates using the migrated-candidates filter when validating reports. (support.greenhouse.io)

Rollback Planning

Greenhouse does not have a native "undo import" feature. Your rollback options:

  • Bulk import tags: Every bulk import gets an auto-generated tag (e.g., "Imported Jun 18, 2025"). Filter by this tag to bulk-reject or delete if needed.
  • API deletion: The Harvest API supports DELETE /candidates/{id} — you can script a cleanup.
  • Keep JazzHR active: Do not cancel your JazzHR subscription until migration is fully validated. Maintain it as read-only for at least 30 days.

Always run at least one full dry run against a Greenhouse test account before the production migration. Import 100 candidates, validate every field, then scale up incrementally.

Limitations and Data Compromises

Be explicit with stakeholders about what cannot transfer 1:1:

JazzHR Capability Greenhouse Limitation
Workflow automations Must be rebuilt as stage transition rules
Questionnaire structure No native questionnaire object; map to custom fields
Interview scheduling history Cannot backdate interviews (scorecards can be backdated)
File-based e-signatures No equivalent; offer letters handled differently
Categories (global labels) Tags exist but are not hierarchical
Per-applicant star ratings No native rating field; use custom fields
Internal messaging No migration path; export as notes if needed
Detailed stage chronology APIs do not document a clean way to backdate every historical stage transition

If the target cannot represent a source artifact natively, store it as a clearly labeled admin-only note rather than dropping it silently.

Best Practices

  1. Back up everything before you start. Export JazzHR's raw data CSV and store it alongside your API extracts. Keep raw source data immutable.
  2. Preserve legacy IDs in Greenhouse. Store applicant_id and appjob_id in custom fields. These are essential for post-migration debugging and reconciliation.
  3. Run test migrations first. At minimum, one full dry run and one targeted rerun before production.
  4. Disable GDPR emails before import. Non-negotiable for historical data.
  5. Pre-create all Greenhouse entities. Jobs, departments, offices, custom fields, sources, and tags must exist before you reference them in import data.
  6. Use the container job strategy for historical data. Do not recreate every closed job from JazzHR.
  7. Budget for the attachment problem. File migration from JazzHR is the #1 time sink. Plan for it explicitly.
  8. Validate incrementally. Do not wait until the full load is complete to start checking data quality.
  9. Archive what you cannot map honestly. Store unmappable data as admin-only notes rather than forcing misleading field mappings.
  10. Do not let middleware become your historical migration engine. iPaaS tools are for ongoing sync, not bulk backfill.

Post-Migration Checklist

  • Rebuild interview plans and stage structures in Greenhouse
  • Configure scorecard templates for each job
  • Set up approval workflows for new job openings
  • Re-create email templates and candidate communication sequences
  • Configure integrations (HRIS, background check, scheduling tools)
  • Train recruiters on the candidate/application model, duplicate handling, and where legacy data lives
  • Re-enable GDPR/CCPA consent email rules
  • Monitor for duplicate candidate alerts over the first 2 weeks
  • Verify migrated data appears correctly in reports (use migrated-candidates filter)
  • Keep the historical container job labeled and excluded from active reporting
  • Maintain JazzHR in read-only mode for at least 30 days

When to Use a Managed Migration Service

Build in-house when:

  • You have fewer than 2,000 candidates
  • No custom fields or questionnaire data to preserve
  • You are comfortable losing attachment linkage
  • A developer is available for 2–4 weeks

Use a managed service when:

  • Your dataset exceeds 5,000 candidates
  • You need attachments migrated reliably
  • You have complex custom field mappings or questionnaire data
  • You cannot afford GDPR/compliance mistakes
  • Your engineering team is already committed to other priorities

The hidden cost of DIY is not the initial script. It is the second and third dry run, the retry queues, the staging database, the reconciliation SQL, and the cutover support when hiring is still live. Most failed migrations are not caused by one dramatic bug — they are caused by dozens of small, documented behaviors that no one engineered around.

For a useful framing of the build-vs-buy trade-off, see Why Data Migration Isn't Implementation.

ClonePartner handles JazzHR-to-Greenhouse migrations by building custom ETL pipelines that bypass the 8,000-row bulk import limit through optimized API batching, migrate attachments directly to candidate profiles without manual ZIP reconciliation, manage GDPR/CCPA compliance triggers so historical candidates do not receive accidental emails, and preserve the full relationship graph from JazzHR's applicant-job model to Greenhouse's candidate-application structure. We run repeatable dry runs with validation before the production cutover, so your recruiting team gets clean data without your engineers being pulled off roadmap work.

Frequently Asked Questions

Can I export resumes from JazzHR via API?
No. JazzHR's GET /files endpoint is locked down for security reasons. You must use the in-app bulk resume download, request a full data export from JazzHR support (which takes 15–20 business days), or use the Candidate Export Webhook which provides documents as base64 on a per-candidate basis with a 2.5-hour URL expiry.
What are the Greenhouse Harvest API rate limits?
The Harvest API allows 50 requests per 10 seconds for v1/v2 integrations. Harvest v3 uses a 30-second window. Exceeding the limit returns an HTTP 429 response with Retry-After and X-RateLimit-Reset headers. Harvest v1/v2 will be removed on August 31, 2026.
Will importing historical candidates into Greenhouse send them emails?
Yes. Greenhouse automatically sends GDPR/CCPA consent emails to candidates imported with a hired or rejected status unless you explicitly disable the consent extension email rules in Settings → Data Privacy before running the import.
What stage history does Greenhouse bulk import support?
Greenhouse's bulk import tool only supports four Milestone values: Application, Assessment, Face to Face, and Offer. Detailed stage-by-stage history is compressed. For full stage fidelity, use the Harvest API to create applications and advance them through stages programmatically.
How long does a JazzHR to Greenhouse migration take?
Using the CSV method, expect 1–2 weeks including data prep and validation. A custom API-based ETL pipeline takes 2–4 weeks of engineering effort. A managed migration service can typically complete the full migration in days, including attachments and validation.

More from our Blog

5
ATS

5 "Gotchas" in ATS Migration: Tackling Custom Fields, Integrations, and Compliance

Don't get derailed by hidden surprises. This guide uncovers the 5 critical "gotchas" that derail most projects, from mapping tricky custom fields and preventing broken integrations to navigating complex data compliance rules. Learn how to tackle these common challenges before they start and ensure your migration is a seamless success, not a costly failure.

Raaj Raaj · · 14 min read
Ensuring GDPR & CCPA Compliance When Migrating Candidate Data
ATS

Ensuring GDPR & CCPA Compliance When Migrating Candidate Data

This is your essential guide to ensuring full compliance with GDPR and CCPA. We provide a 7-step, compliance-first plan to manage your ATS data migration securely. Learn to handle lawful basis, data retention policies, DSARs, and secure transfers to avoid massive fines and protect sensitive candidate privacy.

Raaj Raaj · · 10 min read
In-House vs. Outsourced Data Migration: A Realistic Cost & Risk Analysis
General

In-House vs. Outsourced Data Migration: A Realistic Cost & Risk Analysis

Choosing between in-house and outsourced data migration? The sticker price is deceptive. An internal team might seem free, but hidden risks like data loss, project delays, and engineer burnout can create massive opportunity costs. This realistic analysis compares the true ROI, security implications, and hidden factors of both approaches, giving you a clear framework to make the right decision for your project.

Raaj Raaj · · 8 min read