Pinpoint to Greenhouse Migration: A Technical Guide
A technical guide to migrating from Pinpoint to Greenhouse — covering data model differences, API constraints, object mapping, rate limits, and edge cases.
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 Pinpoint to Greenhouse is a data-model translation problem disguised as a vendor switch. Both systems are application-centric — candidates link to applications, applications link to jobs — but the internals diverge sharply. Greenhouse enforces rigid structured evaluation with mandatory scorecards, attribute-level ratings, and stage-specific interview kits. Pinpoint is built for in-house talent acquisition teams with flexible automation engines, custom workflows, and a lighter evaluation framework.
A naive CSV export from Pinpoint flattens this relational structure, silently drops stage-movement timestamps, collapses multi-application candidate histories into disconnected rows, and strips document attachments entirely. For any dataset over a few hundred records, or where compliance retention matters, you need an API-based migration.
This guide covers the data-model differences, every viable migration method and its trade-offs, the API constraints on both sides, the object-mapping decisions you need to make, and the edge cases that break most DIY attempts.
Disclosure: This guide is published by Clone Partner, a migration services vendor. All technical claims are sourced and verifiable independently.
For related ATS migration patterns, see our Lever to Greenhouse migration guide, BambooHR to Greenhouse migration guide, and ATS migration gotchas.
Greenhouse Harvest API v1 and v2 will be deprecated and unavailable after August 31, 2026. Build your Greenhouse ingestion pipeline against Harvest v3 (OAuth 2.0, cursor-based pagination) from day one. Do not build on v1/v2 only to rewrite later. (support.greenhouse.io)
Why Companies Migrate from Pinpoint to Greenhouse
This move typically happens when a team wants more structure and more ecosystem depth, not when it wants more native modules in a single product.
- Structured hiring enforcement at scale. Greenhouse centers the hiring process around interview kits, scorecards, mandatory feedback loops, governed workflows, and approval chains. Pinpoint's more flexible model doesn't enforce this level of process rigor. Organizations scaling past 500 employees or expanding into regulated industries often need this rigidity.
- Integration ecosystem depth. Greenhouse has 500+ pre-built integrations and a mature partner marketplace. Pinpoint offers over 100 native integrations and an open API, but Greenhouse's ecosystem is significantly deeper for enterprise HRIS, background check, and assessment tool connectivity.
- Standardized reporting and compliance. Greenhouse's EEOC/OFCCP reporting, built-in DEI analytics, and offer-approval audit trails are more mature than Pinpoint's equivalents. For US-headquartered companies with federal compliance obligations, this gap often forces the switch.
That trade-off matters because it tells you what should migrate as live operational data and what can safely become reference history. Pinpoint positions itself as a broader all-in-one system with built-in talent CRM, careers-site tooling, onboarding, background checks, reference checks, and one-way video workflows. Not all of that has a direct Greenhouse equivalent.
Pinpoint vs. Greenhouse: Data Model Differences That Matter
Both systems use an application-centric model, which makes this migration structurally simpler than, say, a Lever-to-Greenhouse move. But the detail-level differences will break your scripts if you don't account for them.
| Concept | Pinpoint | Greenhouse |
|---|---|---|
| Core entity | Application (links Candidate to Job) | Application (links Candidate to Job) |
| Candidate identity | Candidate record with profile data | Candidate record with profile data |
| Evaluation | Scorecards (flexible, per-stage) | Scorecards (rigid, attribute-rated, mandatory per stage) |
| Pipeline stages | Custom per-job, automation-driven | Custom per-job, approval-workflow-driven |
| Custom fields | Custom Attributes + Custom Fields | Custom Fields (keyed to candidate, application, job, or offer) |
| API spec | JSON:API (jsonapi.org) | Standard REST JSON |
| ID format | Integer IDs | Integer IDs |
| Attachments | API-only extraction for documents | Signed S3 URLs (expire after 7 days) |
| Talent pools | CRM-style talent pipelines | Prospect pools (separate from candidate records) |
The Candidate → Application Split
In Greenhouse, the Candidate object represents the person (name, email, phone, social links). The Application object represents that person's candidacy for a specific job (status, stage, source, rejection reason). These are distinct objects with a parent-child relationship.
Pinpoint also separates candidate profiles from applications, which makes the relational translation relatively straightforward. But if a person applied to three jobs in Pinpoint, they must exist as one Candidate in Greenhouse with three Application objects. Do not create three separate Candidate records.
Greenhouse enforces a strict hierarchy that must exist before candidate data is loaded:
- Users — recruiters, hiring managers, coordinators
- Offices & Departments — organizational structure
- Jobs — open or closed roles with stage plans
- Stages — steps within each job's interview plan
If you POST an Application to a Job ID or Stage ID that doesn't exist in Greenhouse, the request fails. Your migration must build the infrastructure before the transactional data.
The JSON:API Spec Wrinkle
Pinpoint's API follows the JSON:API specification (jsonapi.org), which means responses are wrapped in a data object with type, id, and attributes fields, plus a relationships object for linked records. If you're used to flat REST JSON from Greenhouse, you need a deserialization layer.
// Pinpoint response (JSON:API envelope)
{
"data": {
"id": "12345",
"type": "applications",
"attributes": {
"candidate_name": "Jane Doe",
"stage": "Interview",
"created_at": "2025-06-01T12:00:00Z"
},
"relationships": {
"job": { "data": { "type": "jobs", "id": "678" } }
}
}
}// Greenhouse response (standard REST)
{
"id": 12345,
"candidate_id": 67890,
"job_id": 678,
"status": "active",
"current_stage": { "id": 1, "name": "Interview" },
"applied_at": "2025-06-01T12:00:00.000Z"
}Your extraction scripts need to unwrap the JSON:API envelope and resolve relationships via included resources or follow-up requests. To reduce API calls, Pinpoint supports an include parameter — appending ?include=job,answers fetches associated job data and application question responses in a single request. This avoids N+1 query patterns that burn through rate limits.
Pinpoint API Authentication and Key Scoping
Pinpoint uses API key authentication. Keys are generated per-account in the admin dashboard and are bearer tokens passed in the Authorization header (Authorization: Token token=YOUR_API_KEY). There is no OAuth flow on the Pinpoint side — keys are long-lived and do not auto-rotate.
Key scoping implications for migration:
- Admin-grade keys see all records, including confidential jobs and applications from restricted users. A recruiter-scoped key will silently return a partial dataset — your extraction will look complete but miss records.
- Keys do not have granular endpoint permissions — they are account-level. Protect them as secrets; they can read and write all candidate data.
- There is no Pinpoint sandbox environment — all API operations run against production. Test destructive write operations against a staging Greenhouse instance, not against Pinpoint.
Greenhouse Harvest v3 uses OAuth 2.0 with scoped credentials. Request only the scopes your migration pipeline needs. For a read-write migration, you typically need: candidates, applications, jobs, users, custom_fields, activity_feed, attachments.
Pinpoint Pagination Behavior
Pinpoint uses page-number pagination (not cursor-based) across all endpoints. The maximum page [size] is 1,000 records. Key behavior under filter + include combinations:
page [size]=1000applies uniformly across all endpoints, but responses with heavyincludepayloads (e.g.,?include=structured_section_responses.answers,job) can significantly increase response size and latency per page.- Pinpoint does not provide a
total_countin all responses — check themetaobject. Ifmeta.total_pagesis absent for a given endpoint, you must paginate until you receive fewer records thanpage [size]. - Filters and includes can be combined:
?filter [updated_at][gt]=2024-01-01T00:00:00Z&include=job&page [size]=1000&page [number]=2is valid. - There is no cursor token — if records are inserted during extraction, page-number pagination can produce duplicates or gaps. Use
filter [updated_at][gt]anchored to your extraction start timestamp to minimize drift.
Migration Methods: Every Viable Approach and Its Trade-Offs
Method Comparison at a Glance
| Method | Data Fidelity | Attachments | Scorecards | Engineering Effort | Best For |
|---|---|---|---|---|---|
| CSV Export → Bulk Import | Low | No | No | 1–2 days | <500 candidates, no history needed |
| Greenhouse Historical Bulk Import | Medium | No | No | 3–5 days | Closed reference data, searchable archive |
| Full API-to-API (Harvest v3) | High | Yes | Partial | 4–10 weeks | Open candidates, attachments, compliance records |
| Hybrid (recommended) | High for live, medium for archive | Yes (live), No (archive) | Partial | 3–6 weeks | Most teams with mixed active/historical data |
Rough data loss estimates by method:
- CSV/bulk import: ~60–70% of relational and evaluation data lost (stage history, scorecards, attachments, custom field relationships)
- Historical bulk import: ~40–50% data loss (attachments, scorecards, full stage history)
- Full API migration: ~5–15% data loss (scorecard fidelity gap, non-mappable Pinpoint-only artifacts)
- Hybrid: ~10–20% overall data loss depending on what goes to archive vs. API path
These estimates assume a typical dataset with evaluation notes, attachments, and multi-stage pipeline history. A dataset of flat applications with no attachments will see lower loss rates on all methods.
Method 1: CSV Export → Greenhouse Bulk Import
How it works: Export candidate data from Pinpoint using the report builder (CSV, PDF, or Excel) or request an SFTP dump. Import into Greenhouse via their bulk import tool.
What you get: Basic candidate profile data — names, emails, phone numbers, and flat field values.
What you lose:
- Relational structure (candidate → application → job linkage)
- Stage-movement history and timestamps
- Scorecard data and evaluation notes
- Document attachments (resumes, cover letters, offer letters)
- Custom field values that don't map to CSV columns
- Source attribution and referral data
Pinpoint's help documentation confirms that CSV exports cannot contain embedded files. Resumes and documents export as a separate folder and must be manually re-attached per candidate. At scale, this is a non-starter.
When to use it: Only for very small datasets (<500 candidates) where historical hiring data is expendable.
Method 2: Greenhouse Historical Bulk Import
Greenhouse offers a documented historical bulk import path that's distinct from CSV. It supports application status, stage mapping for open jobs, rejection reason mapping, and key dates (application, rejection, hired). Each import handles up to 8,000 rows. (support.greenhouse.io)
This is faster than a full API rebuild, but comes with constraints:
- Greenhouse warns that historical bulk-imported data can be used as reference, but reporting may be limited
- Hired or rejected imports can automatically trigger GDPR/CCPA consent emails unless those rules are disabled first
- No attachment import, no scorecard data, limited custom field support
- Greenhouse recommends a container job such as HISTORICAL DATA for consolidating old records, marked as a template so it's excluded from reports
When to use it: For closed/archived candidates where you need searchable reference data but don't need full reporting fidelity.
Method 3: Full API-to-API Migration (Harvest v3)
How it works: Extract data from Pinpoint's REST API, transform in a staging layer, and write into Greenhouse via the Harvest v3 API.
What you get: Full relational data — candidates, applications, jobs, notes, attachments, custom fields, stage placements.
What you lose: Some evaluation fidelity. Pinpoint's scorecard format is more flexible than Greenhouse's rigid attribute-rated scorecards. You'll need to decide whether to map scores into Greenhouse scorecard attributes or serialize them as structured notes.
When to use it: Any migration where preserving hiring history, attachments, and relational integrity matters.
Method 4: Hybrid Approach (Recommended for Most Teams)
For most teams, combining methods produces the best result:
- Historical bulk import for closed reference data (rejected/hired candidates from older requisitions — typically records with no activity in the past 12–18 months and no pending compliance obligations)
- Harvest v3 API for open or recent candidates, prospects, attachments, notes, and anything requiring controlled stage placement
- External archive for Pinpoint-only artifacts that don't map cleanly into Greenhouse (onboarding data, one-way video recordings, background checks, rich interview history)
In practice, for a dataset of 10,000 total candidates: roughly 60–70% typically qualify for bulk import (closed, no attachments, older than 12 months), 25–35% require the API path (open, recent, or with attachments), and 5–10% belong in external archive only (Pinpoint-native artifacts with no Greenhouse equivalent).
This is less elegant than a full native rebuild, but it's more honest and more reliable. It keeps the target system clean without pretending every source object deserves a forced one-to-one translation.
API Constraints That Shape Your Pipeline
Both Pinpoint and Greenhouse enforce rate limits that directly affect your migration timeline and architecture.
Pinpoint API
| Constraint | Value |
|---|---|
| Requests per IP per ~1 minute | 120 |
| Requests per IP per ~8 minutes | 240 |
| Throttle response | HTTP 429 |
| Reset headers | RateLimit-Limit, RateLimit-Reset, Retry-After |
| Max page size | 1,000 records |
| Confidential jobs | Excluded unless confidential is in the job visibility filter |
At 120 requests per minute, extracting 10,000 candidates with related applications, notes, and documents will take hours. Each candidate may require 3–5 follow-up requests to resolve relationships (applications, scorecards, attachments), so plan for 30,000–50,000 total API calls on the extraction side.
Key extraction details:
- Attachments require
extra_fields [applications]=attachments— they aren't returned by default - Scorecards have their own endpoint and must be pulled separately
- Report builder exports are visibility-dependent — a restricted service user produces a partial dataset even if your queries look correct
- PDF export is feature-gated, limited to 100 applications per batch, and unavailable for anonymized applications — use it for audit bundles, not primary extraction
Common Pinpoint API Error Signatures
| Error | HTTP Status | Likely Cause | Resolution |
|---|---|---|---|
| Rate limit exceeded | 429 | >120 req/min | Read Retry-After header; exponential backoff |
| Missing attachment data | 200 (empty attachments array) |
Missing extra_fields [applications]=attachments param |
Add param to request |
| Partial dataset returned | 200 (lower count than expected) | Recruiter-scoped key; confidential jobs excluded | Switch to admin key; add filter [job_visibility]=confidential,external,internal,private_job |
| Anonymized record | 200 (redacted fields) | GDPR deletion applied | Do not re-create in Greenhouse; log and skip |
| Relationship not resolved | 200 (null relationship) | Record deleted or permissions gap | Log the orphaned record; investigate before migrating |
Greenhouse Harvest API
| Constraint | Value |
|---|---|
| v1/v2 rate limit | ~50 requests per 10 seconds (varies by key type) |
| v3 rate limit | Returned in X-RateLimit-Limit header; varies by endpoint and tier |
| Throttle response | HTTP 429 with X-RateLimit-Reset and Retry-After headers |
| Max page size (v3 list endpoints) | 500 records with cursor pagination |
| v1/v2 sunset | August 31, 2026 |
Greenhouse does not publish a single universal rate limit for v3. The limit is returned in the X-RateLimit-Limit response header and may vary by endpoint and integration tier. Always read the header dynamically rather than hardcoding a value.
Greenhouse Sandbox Environment
Greenhouse provides sandbox environments for testing. To request one, contact your Greenhouse Customer Success Manager or submit a request through the Greenhouse support portal. Sandbox instances are separate from production — they have separate API credentials, separate data, and do not share rate limit quotas with production.
Best practices for sandbox testing:
- Load a representative sample of ~200 records covering all edge cases (multi-application candidates, confidential jobs, candidates with attachments, custom field types)
- Run your full write pipeline against sandbox before touching production
- Verify custom field values, attachment accessibility, and duplicate detection behavior before scaling up
- Sandbox data does not persist indefinitely — confirm retention period with your CSM before building long validation workflows
Practical Throughput Math
For a 10,000-candidate migration with an average of 2 applications per candidate:
- Pinpoint extraction: ~50,000 API calls ÷ 120/min ≈ 7 hours (with backoff)
- Greenhouse loading: ~30,000 write calls (candidates + applications + notes + attachments) ≈ 3–4 hours at effective v3 throughput
- Total API time: ~10–12 hours, not counting transformation or error handling
Build your pipeline with exponential backoff, idempotency keys, and checkpoint/resume logic. A network failure at hour 8 should not restart the entire migration.
Object Mapping: Pinpoint → Greenhouse Field-by-Field
Candidates
| Pinpoint Field | Greenhouse Target | Notes |
|---|---|---|
candidate.name |
first_name + last_name |
Pinpoint may store as single field — split required |
candidate.email |
email_addresses [] |
Greenhouse supports multiple typed emails |
candidate.phone |
phone_numbers [] |
Greenhouse requires type (mobile, home, work) |
| LinkedIn URL | social_media_addresses [] |
Do not put in a custom field. Greenhouse duplicate detection uses email, phone, and LinkedIn URL — LinkedIn matching depends on data landing in the social media field family. (support.greenhouse.io) |
| Source attribution | source on Application |
Map Pinpoint channels to Greenhouse source IDs via GET /v3/sources |
| Custom attributes | custom_fields{} |
Requires pre-creating matching custom fields in Greenhouse |
| Talent pool membership | Prospect record | Map to Greenhouse prospects, not fake rejected applications |
Applications and Stages
| Pinpoint Field | Greenhouse Target | Notes |
|---|---|---|
| Application stage | current_stage / initial_stage_id |
Requires stage ID lookup in Greenhouse |
| Application status | Application status | Use POST /reject or POST /hire endpoints |
| Stage timestamps | Activity feed notes | Greenhouse doesn't support backdating stage movements |
| Rejection reason | rejection_reason |
Map to Greenhouse rejection reason IDs |
Greenhouse does not support backdating stage movements via the API. If a candidate moved through 5 stages in Pinpoint, you can place them in their final stage, but you cannot recreate the step-by-step progression with historical timestamps. Serialize the full stage history as a structured note on the candidate's activity feed.
Stage transitions after initial placement are handled by dedicated lifecycle endpoints (move, reject, hire, unreject) — not by patching the application directly. The move endpoint requires from_stage_id, which acts as a stale-write guard but is easy to mishandle during parallel operations. (harvestdocs.greenhouse.io)
Scorecards and Evaluations
This is where the models diverge most. Pinpoint uses flexible per-stage scorecards. Greenhouse uses structured scorecards with attribute-level ratings (e.g., "Strong Yes" to "Strong No" on specific competencies).
Greenhouse's public note creation endpoint supports only NOTE, EMAIL, and ACTIVITY as creatable note types via the Harvest API. (support.greenhouse.io) Interview or feedback note types (scorecards) are produced by Greenhouse's own interview kit features, not by the write API — you cannot inject a scorecard via API as a native Greenhouse scorecard object.
Your options:
- Map to Greenhouse scorecards — only if Pinpoint scorecards have directly comparable attributes. Requires pre-configuring interview kits and scorecard templates in Greenhouse. Validate in a sandbox before assuming this works at scale.
- Serialize as notes — convert Pinpoint scorecard data into formatted text notes on the candidate's activity feed. Preserves the information but loses queryability and structured reporting.
- Hybrid — map numeric scores to Greenhouse attributes where possible, append free-text feedback as notes.
Most migrations use option 2 or 3. Full scorecard mapping requires significant manual Greenhouse configuration and rarely produces a perfect translation.
{
"user_id": 555666,
"body": "[Migrated from Pinpoint - 2023-10-14] Interviewer: John Smith\nRating: 4/5\nFeedback: Candidate showed excellent system design skills but struggled with direct API mapping questions.",
"visibility": "private"
}If the original interviewer has left your company and doesn't exist in Greenhouse, attribute the note to a generic "Migration Admin" user account to prevent API rejection.
Attachments and Documents
Pinpoint's API is the only reliable method for extracting documents. CSV exports cannot contain embedded files. Attachments require extra_fields [applications]=attachments in your API request — they're not included by default. (developers.pinpointhq.com)
On the Greenhouse side, write attachments using POST /candidates/{id}/attachments. Critical constraints:
- Greenhouse stores attachments on S3 with signed URLs that expire after 7 days. If you ever need to re-extract from Greenhouse, download immediately after upload and verify.
- Uploading a new
resumeorcover_letterreclassifies the earlier one asother. This matters if Pinpoint carries multiple resume versions per candidate. - Greenhouse can ingest files via base64
contentor by fetching a publicurl— Pinpoint URLs require authentication, so you must download to staging and re-upload as base64. - Size limit: 20MB per file. Filter out oversized portfolio files or store them externally with a link in a Greenhouse candidate note.
The migration workflow:
- Extract document metadata from Pinpoint via API (with
extra_fields) - Download each binary file to staging storage
- Upload to Greenhouse via the attachments endpoint with the base64 payload
- Verify the attachment is accessible on the candidate record before the signed URL expires
Budget significant time here. A 10,000-candidate dataset with 2 documents per candidate means 20,000 file transfers, each consuming an API call on both sides.
Custom Fields
Pinpoint custom attributes are typed and bound to a specific resource_type. Greenhouse custom fields follow the same pattern — fields attach to a defined resource (candidate, application, job, or offer), and select fields depend on option dictionaries.
In Greenhouse v3, name_key is the stable identifier you should anchor mappings to, not the human-readable label that admins can rename. Select options live in /v3/custom_field_options. Mapping by display text alone is a common source of import breakage.
To pre-create a custom field in Greenhouse via API:
POST /v3/custom_fields
{
"name": "Pinpoint Source Campaign",
"field_type": "application",
"value_type": "short_text",
"private": false,
"required": false
}The response returns the name_key (e.g., pinpoint_source_campaign) — anchor all downstream mappings to this value, not the name. For select-type fields, you must also create option values via POST /v3/custom_field_options before any application data referencing those options will load without error.
One more edge case: Greenhouse candidate education records expect option IDs for school, degree, and discipline, while employment history uses free-text company and title.
Audit both systems' custom field schemas before writing a single line of migration code. A multi-select field in Pinpoint may need to become multiple single-select fields in Greenhouse, or you may lose option values that don't exist in the target configuration.
Crosswalk Table Schema
For any migration involving a dual-run window or incremental delta replays, maintain an idempotency crosswalk table in your staging layer:
CREATE TABLE migration_crosswalk (
id SERIAL PRIMARY KEY,
source_system VARCHAR(50) NOT NULL, -- 'pinpoint'
source_object VARCHAR(50) NOT NULL, -- 'candidate', 'application', 'attachment'
source_id VARCHAR(100) NOT NULL, -- Pinpoint integer ID as string
target_system VARCHAR(50) NOT NULL, -- 'greenhouse'
target_object VARCHAR(50) NOT NULL, -- 'candidate', 'application', 'attachment'
target_id VARCHAR(100), -- Greenhouse ID once created
status VARCHAR(20) NOT NULL, -- 'pending', 'success', 'failed', 'skipped'
error_message TEXT, -- API error body if failed
last_sync_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (source_system, source_object, source_id)
);Before every write operation, check the crosswalk table. If status = 'success' and target_id is populated, skip the write. This prevents duplicate records if your pipeline crashes and restarts, and provides an audit trail for validation.
Archive vs. Migrate Decision Framework
Not all Pinpoint data belongs in Greenhouse. Use this decision tree for each record category:
For each candidate/application record, ask:
1. Is this record subject to a deletion request (GDPR Article 17, CCPA)?
→ YES: Delete in Pinpoint. Do NOT migrate. Log deletion.
→ NO: Continue.
2. Is the candidate currently active (open application or prospect)?
→ YES: Migrate via Harvest v3 API. Preserve attachments and notes.
→ NO: Continue.
3. Was the candidate hired or rejected within the past 24 months?
→ YES: Migrate via Greenhouse historical bulk import (searchable reference).
→ NO: Continue.
4. Does the record have evaluation data (scorecards, notes) or attachments?
→ YES: Migrate via Harvest v3 API if within retention window; otherwise archive externally.
→ NO: Continue.
5. Is the candidate older than your retention policy limit (commonly 2 years for EU, varies by jurisdiction)?
→ YES: Delete per policy. Do NOT migrate.
→ NO: Migrate via Greenhouse historical bulk import as reference data.
6. Is the artifact Pinpoint-only (onboarding task, video recording, background check)?
→ YES: Archive externally. Do not force into Greenhouse.
Jurisdiction thresholds to apply:
- EU/EEA under GDPR: Typical retention limit for rejected candidates is 6 months to 2 years depending on stated purpose at collection
- US federal contractors (OFCCP): 2-year retention required for applicant records
- UK post-Brexit: ICO guidance mirrors GDPR with similar timeframes
- Apply the shortest applicable retention window when multiple jurisdictions apply
Edge Cases That Break DIY Migrations
Duplicate Candidate Detection
Greenhouse deduplicates candidates by email address, phone number, and LinkedIn URL. If a candidate in Pinpoint has multiple email addresses, or if their email changed between systems, you may create duplicates. Pre-deduplicate your dataset before writing to Greenhouse, and use Greenhouse's candidate merge endpoint to clean up any that slip through.
The Greenhouse merge endpoint: PUT /v3/candidates/{id}/merge. Required fields: bad_candidate_id (the duplicate to absorb) and good_candidate_id (the canonical record to keep). The good_candidate_id retains all applications; data from bad_candidate_id that doesn't conflict is merged in. Conflicting fields (e.g., two different primary emails) favor the good_candidate_id values.
Talent Pool → Prospect Conversion
Pinpoint's talent pipeline CRM stores candidates who haven't applied to a specific job. In Greenhouse, these map to Prospects — candidates who exist without an active application. Use the Harvest API's prospect creation endpoint. Creating jobless prospects requires specific Greenhouse user permissions.
Do not fabricate rejected applications just to preserve a name in the database. Greenhouse prospects are the correct abstraction.
Confidential Jobs
Pinpoint excludes confidential jobs from API responses unless you explicitly include confidential in the job visibility filter. Missing this produces a partial extraction that looks complete. Run all extracts with an admin-grade account and the visibility filter enabled:
GET /api/v1/applications?filter[job_visibility]=confidential,external,internal,private_job
Custom Field Type Mismatches
Pinpoint and Greenhouse support different custom field data types. Common mismatches:
| Pinpoint Type | Greenhouse Equivalent | Migration Note |
|---|---|---|
| Multi-select | No direct equivalent | Split into multiple boolean fields or concatenate as text |
| Free-text long | long_text |
Direct mapping — verify character limits |
| Date | date |
Reformat to ISO 8601 if not already |
| File attachment | No custom field equivalent | Store as candidate attachment, not custom field |
| Number | number |
Direct mapping |
Map every field individually, and test in a sandbox before running the production load.
GDPR and Data Retention
Both systems support GDPR compliance workflows. Pinpoint holds ISO 27001 and SOC 2 Type II certifications and encrypts data at rest with AES-256. During migration, candidate data transits through your ETL pipeline — ensure your staging layer meets the same compliance bar.
Critical rules:
- If candidates have exercised data deletion rights in Pinpoint, do not re-create those records in Greenhouse
- Do not migrate your entire 10-year history if your retention policy requires purging rejected candidates after 2 years — use date filters in your extraction
- Greenhouse historical bulk import can automatically send GDPR/CCPA consent emails for hired/rejected records — disable those email rules before import if the notifications would be inappropriate (support.greenhouse.io)
Pinpoint Data Retention After Termination
Pinpoint removes all data within 15 days of contract termination. If you're running your migration close to your contract end date, complete the extraction before your account is deactivated. There is no grace period for API access after termination.
Webhook Events During Cutover
If you're running both systems in parallel during transition, Pinpoint webhooks (new applications, stage changes, offer accepted) will continue firing. Pinpoint webhook payload schema includes:
{
"event": "application.stage_changed",
"timestamp": "2025-06-01T12:00:00Z",
"data": {
"application_id": 12345,
"candidate_id": 67890,
"job_id": 678,
"from_stage": "Phone Screen",
"to_stage": "Interview",
"changed_at": "2025-06-01T11:58:00Z"
}
}Pinpoint webhook receivers have a 5-second timeout, only 2XX responses count as success, and Pinpoint retries up to 10 times over 24 hours with exponential backoff and jitter. (developers.pinpointhq.com)
For delta replay during the dual-run window, consume these webhook events and look up the corresponding Greenhouse target ID in your crosswalk table. Use the from_stage_id from your crosswalk (the last confirmed Greenhouse stage) to construct the Greenhouse move API call — this prevents stale-write errors if the webhook fires twice.
Decide on a clean cutover date. Disable Pinpoint job postings before that date to avoid split-brain data where candidates exist in both systems with different statuses.
Step-by-Step Migration Workflow
Step 1: Audit and Inventory
Before writing code, document:
- Total candidate count (active + archived)
- Total application count across all jobs
- Custom field definitions and data types in both systems
- Pipeline stages per job
- Document/attachment volume and estimated total size
- Active integrations that need reconnecting in Greenhouse
- Data retention obligations (GDPR, EEOC, local regulations)
- Classify every source artifact using the archive-vs-migrate decision framework above: native in Greenhouse via API, bulk import reference, or archive only
Step 2: Configure the Greenhouse Target
Set up Greenhouse before loading any data:
- Create departments, offices, and job structures
- Configure pipeline stage plans mapped to Pinpoint stages
- Create custom fields via
POST /v3/custom_fields, anchoring mappings toname_key - Create select-option values via
POST /v3/custom_field_optionsfor any select-type fields - Set up rejection reason taxonomies
- Create interview kits and scorecard templates if mapping evaluation data
- Generate Harvest v3 API credentials with appropriate endpoint permissions
- If using historical bulk import, create a container job (e.g.,
HISTORICAL DATA) and mark it as a template to exclude from reports - Request a Greenhouse sandbox environment and validate your full write pipeline before touching production
Step 3: Build the Extraction Pipeline
Connect to Pinpoint's API at https://{subdomain}.pinpointhq.com/api/v1. Use an admin-grade API key. Extract in dependency order:
- Jobs — all jobs with stage configurations, including confidential
- Candidates — profiles with custom attributes
- Applications — with stage, status, timestamps, and
extra_fields [applications]=attachments - Scorecards — pulled separately via the scorecards endpoint
- Documents — download all attachments to staging storage
# Example Pinpoint extraction queries
GET /api/v1/applications?include=structured_section_responses.answers&extra_fields[applications]=attachments&filter[updated_at][gt]=2024-01-01T00:00:00Z&filter[job_visibility]=confidential,external,internal,private_job&page[size]=1000&page[number]=1
GET /api/v1/scorecards?filter[created_at][gt]=2024-01-01T00:00:00Z&page[size]=1000&page[number]=1Paginate until response count < page [size]. Implement exponential backoff on 429 responses and read the Retry-After header. Log every response to your crosswalk table.
Step 4: Transform and Validate
- Unwrap JSON:API envelopes into flat objects
- Split combined name fields into
first_name/last_name - Map Pinpoint stage names to Greenhouse stage IDs
- Map Pinpoint sources to Greenhouse source IDs via
GET /v3/sources - Convert custom field values to match Greenhouse field types and option IDs (by
name_key, not display label) - Deduplicate candidates by email — flag multi-email records for manual review
- Route LinkedIn URLs to Greenhouse
social_media_addresses [], not custom fields - Flag records missing required Greenhouse fields (e.g., no email address)
- Serialize scorecard data into structured note format
- Apply the archive-vs-migrate decision framework; exclude records flagged for deletion
Step 5: Load into Greenhouse
Write in dependency order via Harvest v3:
- Candidates —
POST /v3/candidateswith profile data and initial application - Applications — additional applications linked to pre-created jobs
- Stage placement — use lifecycle endpoints for stage moves
- Notes —
POST /candidates/{id}/activity_feed/notesfor historical data including serialized stage history and scorecard content - Attachments —
POST /candidates/{id}/attachmentswith base64 payloads - Status updates —
POST /applications/{id}/rejectorPOST /applications/{id}/hire
For each write, record the resulting Greenhouse ID in your crosswalk table with status = 'success'. On failure, record the error body and status = 'failed' — do not retry blindly; investigate the error signature first.
For historical bulk import records, load them separately with GDPR email rules disabled.
Step 6: Run a Dual-Run Window (If Needed)
For open requisitions, subscribe to Pinpoint webhook events (application.created, application.stage_changed, offer.accepted). Queue events and look up crosswalk entries to replay deltas into Greenhouse using idempotent operations. Use the last recorded Greenhouse stage from your crosswalk as the from_stage_id for move operations — this prevents stale-write errors if a webhook fires more than once.
Step 7: Validate and Cut Over
- Compare record counts by job, status, and stage between source and target
- Spot-check 50–100 records for field-level accuracy across active, rejected, hired, prospect, and confidential records
- Open migrated attachments and verify they're accessible (before signed URLs expire — do this within 7 days of upload)
- Confirm custom field values persisted correctly (check
name_keymappings, not display labels) - Check rejection and hire dates, null custom fields, and duplicate detection behavior
- Run the Greenhouse merge endpoint on any confirmed duplicates
- Freeze Pinpoint writes, replay the final delta batch using the crosswalk table to identify remaining
pendingrecords, switch inbound integrations to Greenhouse - Keep Pinpoint read-only until stakeholders sign off on counts and spot checks
Choosing the Right Greenhouse API
Greenhouse offers multiple APIs. Picking the wrong one is a common mistake.
| API | Use Case for Migration |
|---|---|
| Harvest v3 | Primary migration API. Full read/write access to candidates, applications, notes, attachments, custom fields. OAuth 2.0. |
| Candidate Ingestion | Designed for sourcing partners. Can create candidates/prospects but lacks scorecard writes, attachment uploads, and deep custom field support. |
| Job Board | Public, read-only. Not useful for migration. |
Harvest v3 is the correct choice. The Candidate Ingestion API is tempting because of its simpler endpoint, but it lacks the depth needed for full historical data migration.
What This Costs in Engineering Time
| Dataset Size | DIY Engineering Time | Risk Level |
|---|---|---|
| <1,000 candidates | 2–3 weeks | Medium — custom field mapping is still complex |
| 1,000–10,000 candidates | 4–6 weeks | High — rate limits, attachments, and edge cases compound |
| 10,000+ candidates | 6–10 weeks | Very high — needs checkpoint/resume, dedup, and validation infrastructure |
Calendar time often exceeds engineering time because you're waiting on rate limits, coordinating cutover windows with recruiting teams, and iterating on data validation.
When Not to Migrate Everything
Not all Pinpoint data belongs in Greenhouse. Use the archive-vs-migrate decision framework above to classify each record category. Common categories that belong in archive, not Greenhouse:
- Candidates older than your retention policy limit with no activity — GDPR and OFCCP retention limits may require deletion, not migration
- Rejected candidates with no evaluation data — importing thousands of dead records clutters Greenhouse and may slow search
- Onboarding data — Pinpoint's onboarding module data (offer letters, tasks) may not have a Greenhouse equivalent; archive separately
- One-way video interview recordings — too large for API transfer and may have separate retention obligations
- Background check and reference check artifacts — these often have their own retention rules and may live better in the vendor's own system
Making the Call
Pinpoint to Greenhouse is one of the more structurally aligned ATS migrations — both systems are application-centric, both use integer IDs, and both have documented APIs. The hard parts are Pinpoint's tight rate limits (120/min), the JSON:API deserialization overhead, attachment handling at scale, and scorecard fidelity translation.
For most teams, the right answer is a hybrid plan: Greenhouse historical bulk import for closed reference history (roughly 60–70% of a typical dataset), Harvest v3 for live or open candidates and anything requiring controlled stage placement (25–35%), and a separate archive for Pinpoint-only artifacts that don't map into Greenhouse's documented surfaces (5–10%). That keeps the target system clean without pretending every source object deserves a forced one-to-one rewrite.
If you have a clean dataset under 1,000 candidates and don't need historical evaluation data, a focused engineering sprint can handle this in a couple of weeks. For anything larger — or where compliance, attachments, and data integrity matter — the compounding edge cases (duplicate detection, custom field type mismatches, GDPR data-in-transit, parallel cutover, crosswalk integrity) make this a project that benefits from someone who's done it before.
For broader migration planning, see our ATS data migration checklist.
Frequently Asked Questions
- Can I migrate from Pinpoint to Greenhouse using a CSV export?
- Only for very small datasets under 500 candidates where you don't need historical hiring data. CSV exports flatten relational structures, drop stage-movement timestamps, and cannot include document attachments. Pinpoint's help docs confirm that resumes and documents export separately and must be manually re-attached. For any production migration, use the API-to-API approach or Greenhouse's historical bulk import.
- Does Greenhouse support backdating stage movements during migration?
- No. Greenhouse does not support backdating stage movements via the API. You can place candidates in their final stage, but you cannot recreate the step-by-step progression with historical timestamps. Serialize the full stage history as a structured note on the candidate's activity feed.
- Which Greenhouse API should I use for a Pinpoint migration?
- Use the Harvest v3 API (OAuth 2.0). It provides full read/write access to candidates, applications, notes, and attachments. The Candidate Ingestion API is simpler but lacks scorecard writes, attachment uploads, and deep custom field support. Harvest v1/v2 will be unavailable after August 31, 2026.
- How long does a Pinpoint to Greenhouse migration take?
- For under 1,000 candidates, expect 2–3 weeks of engineering time. For 1,000–10,000, plan 4–6 weeks. Over 10,000 candidates typically takes 6–10 weeks due to rate limits, attachment handling, deduplication, and validation. Calendar time often exceeds engineering time due to cutover coordination.
- How do I avoid missing confidential jobs during Pinpoint extraction?
- Pinpoint excludes confidential jobs from API responses unless you explicitly include 'confidential' in the job visibility filter. Run extracts with an admin-grade account because Pinpoint report exports are also visibility-dependent — a restricted user will produce a partial dataset.