---
title: "Pinpoint to Greenhouse Migration: A Technical Guide"
slug: pinpoint-to-greenhouse-migration-a-technical-guide
date: 2026-08-20
author: Nachi
categories: [Pinpoint, Greenhouse, Migration Guide]
excerpt: "A technical guide to migrating from Pinpoint to Greenhouse — covering data model differences, API constraints, object mapping, rate limits, and edge cases."
tldr: "Pinpoint to Greenhouse is structurally aligned but complicated by tight API rate limits, JSON:API deserialization, and scorecard fidelity. Use Harvest v3 and a hybrid import approach."
canonical: https://clonepartner.com/blog/pinpoint-to-greenhouse-migration-a-technical-guide/
---

# Pinpoint to Greenhouse Migration: A Technical Guide


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](https://clonepartner.com/blog/blog/lever-to-greenhouse-migration-the-ctos-technical-guide/), [BambooHR to Greenhouse migration guide](https://clonepartner.com/blog/blog/bamboohr-to-greenhouse-migration-the-ctos-technical-guide/), and [ATS migration gotchas](https://clonepartner.com/blog/blog/ats-migration-gotchas/).

> [!WARNING]
> 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](https://support.greenhouse.io/hc/en-us/articles/360029266032-Harvest-API-overview))

## 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](https://clonepartner.com/blog/blog/lever-to-greenhouse-migration-the-ctos-technical-guide/). 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:

1. **Users** — recruiters, hiring managers, coordinators
2. **Offices & Departments** — organizational structure
3. **Jobs** — open or closed roles with stage plans
4. **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.

```json
// 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" } }
    }
  }
}
```

```json
// 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]=1000` applies uniformly across all endpoints, but responses with heavy `include` payloads (e.g., `?include=structured_section_responses.answers,job`) can significantly increase response size and latency per page.
- Pinpoint does not provide a `total_count` in all responses — check the `meta` object. If `meta.total_pages` is absent for a given endpoint, you must paginate until you receive fewer records than `page [size]`.
- Filters and includes can be combined: `?filter [updated_at][gt]=2024-01-01T00:00:00Z&include=job&page [size]=1000&page [number]=2` is 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

> [!CAUTION]
> 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](https://support.greenhouse.io/hc/en-us/articles/38329467264027-Adding-historical-candidates-to-Greenhouse-using-bulk-import))

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 |

([developers.pinpointhq.com](https://developers.pinpointhq.com/docs/introduction))

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

([developers.pinpointhq.com](https://developers.pinpointhq.com/reference/get-applications))

### 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 |

([support.greenhouse.io](https://support.greenhouse.io/hc/en-us/articles/360029266032-Harvest-API-overview))

> [!NOTE]
> 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](https://support.greenhouse.io/hc/en-us/articles/49207850415003-Configure-the-Duplicate-tag)) |
| 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 |

> [!WARNING]
> 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](https://harvestdocs.greenhouse.io/reference/patch_v3-applications-id))

### 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](https://support.greenhouse.io/hc/en-us/articles/360029266032-Harvest-API-overview)) 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:

1. **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.
2. **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.
3. **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.

```json
{
  "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](https://developers.pinpointhq.com/reference/get-application))

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 `resume` or `cover_letter` reclassifies the earlier one as `other`.** This matters if Pinpoint carries multiple resume versions per candidate.
- **Greenhouse can ingest files via base64 `content` or by fetching a public `url`** — 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:

1. Extract document metadata from Pinpoint via API (with `extra_fields`)
2. Download each binary file to staging storage
3. Upload to Greenhouse via the attachments endpoint with the base64 payload
4. 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:

```json
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:

```sql
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
```

([developers.pinpointhq.com](https://developers.pinpointhq.com/reference/get-applications))

### 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](https://support.greenhouse.io/hc/en-us/articles/38329467264027-Adding-historical-candidates-to-Greenhouse-using-bulk-import))

### Pinpoint Data Retention After Termination

> [!NOTE]
> 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:

```json
{
  "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](https://developers.pinpointhq.com/docs/webhooks-overview))

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 to `name_key`
- Create select-option values via `POST /v3/custom_field_options` for 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:

1. **Jobs** — all jobs with stage configurations, including confidential
2. **Candidates** — profiles with custom attributes
3. **Applications** — with stage, status, timestamps, and `extra_fields [applications]=attachments`
4. **Scorecards** — pulled separately via the scorecards endpoint
5. **Documents** — download all attachments to staging storage

```bash
# 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]=1
```

Paginate 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:

1. **Candidates** — `POST /v3/candidates` with profile data and initial application
2. **Applications** — additional applications linked to pre-created jobs
3. **Stage placement** — use lifecycle endpoints for stage moves
4. **Notes** — `POST /candidates/{id}/activity_feed/notes` for historical data including serialized stage history and scorecard content
5. **Attachments** — `POST /candidates/{id}/attachments` with base64 payloads
6. **Status updates** — `POST /applications/{id}/reject` or `POST /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_key` mappings, 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 `pending` records, 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](https://clonepartner.com/blog/blog/the-ultimate-ats-data-migration-checklist-a-10-point-plan-for-a-compliant-transition/).

> Need help migrating from Pinpoint to Greenhouse without losing candidate history, documents, or compliance-sensitive records? We'll review your dataset, map the objects, and give you an honest timeline — free, 30 minutes, no commitment.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## 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.
