> For the complete documentation index, see [llms.txt](https://docs.goodfit.io/goodfit-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.goodfit.io/goodfit-docs/apis/enrichment-api/contacts.md).

# Contacts

The Contact Enrichment API takes a single person you already have a handle on and returns their verified identity, current employment, work email and, optionally, a mobile phone number.

Persona searches find contacts for you across a whole segment. Use this API when you already know exactly who you want, such as a webform signup, a name from an event list or an existing record in your CRM, and you just need the rest of their details.

The contact is added to your Goodfit dataset and can be synced straight back to your CRM, so the person stays up to date alongside everyone else Goodfit maintains for you.

Identify the person with `identity.linkedInUrl`, `identity.workEmail`, or `identity.firstName` + `identity.lastName` + a company (`identity.companyDomain` or `identity.companyName`). Everything else is optional. A name is only tried when no LinkedIn URL or work email was given, or when they found no one. **Matching on a name with no company is not supported.**

This is an employment API. It returns a person **at a company**, not a standalone person record. Job title, start date and work email all describe that specific employment.

### Common use cases

#### Enrich webform signups

Fill in the full details of people who reach you through forms, demo requests or product signups.

A typical flow is:

1. Someone submits a form with their work email.
2. Submit their `workEmail`, and `companyDomain` if you have it, to the Contact Enrichment API.
3. Store the returned `requestId`.
4. Check the request status until it is `COMPLETE` or `ERROR`.
5. Use the result in your workflow, or let Goodfit write it to your CRM.

#### Enrich contacts created directly in your CRM

Contacts created directly in Salesforce or HubSpot can be enriched and written back to the same record.

Provide the record ID using `externalIds` and set `syncToCRM: true`. Goodfit then updates that exact record rather than creating a second contact. See Syncing to your CRM.

Supported external IDs are:

* `salesforce`: Salesforce contact ID
* `hubspot`: HubSpot contact ID

Salesforce IDs can be either 15 or 18 characters; 15 character IDs are expanded to 18. HubSpot IDs must be numeric.

The corresponding CRM connector must already be enabled for your Goodfit account.

#### Look up people from an event list

Enrich attendees and leads collected at events and webinars. Submit each person with their `linkedInUrl` or `workEmail`, plus `companyDomain` where you have it, and poll each `requestId`. If you only have names, send `firstName`, `lastName` and `companyDomain` (or `companyName`), plus `jobTitle` if you have it.

For longer lists, pace your requests to stay within the [rate limit](https://docs.goodfit.io/goodfit-docs/apis/apis-introduction#rate-limits).

### Integration patterns

#### Asynchronous enrichment

Contact enrichment is asynchronous, because finding and verifying someone's details takes longer than a single HTTP request should wait.

A successful request returns a `requestId` immediately. Use that `requestId` to check the status of the request until processing finishes.

Requests typically complete within **one to two minutes**.

The possible status values are:

| Status        | Description                                               |
| ------------- | --------------------------------------------------------- |
| `QUEUED`      | Request is accepted but not started yet                   |
| `IN_PROGRESS` | Request is being enriched                                 |
| `COMPLETE`    | Enrichment finished; `result` is present                  |
| `ERROR`       | Enrichment failed; `error` explains why. Safe to resubmit |

A typical integration flow is:

1. Submit a person using `POST /enrichment/enrich-contact/request`.
2. Store the returned `requestId`.
3. Poll `GET /enrichment/enrich-contact/request/{requestId}` every few seconds.
4. Continue polling while the status is `QUEUED` or `IN_PROGRESS`.
5. When the status is `COMPLETE`, check `result.found`. `COMPLETE` also covers requests where Goodfit couldn't resolve the person.
6. For `ERROR`, inspect `error`.

If you asked for a CRM sync, that follows once the enrichment completes.

#### Send the company whenever you have it

Someone can hold more than one role. Sending `companyDomain` on the first request identifies the right employment in almost all cases. Without it, a person with several roles comes back as `MULTIPLE_EMPLOYMENTS_FOUND` and you pick one, either by sending your choice to the original request or by resubmitting. See [Handling multiple employments](https://docs.goodfit.io/goodfit-docs/apis/enrichment-api/contacts/handling-multiple-employments).

#### Retry safely

Submitting the same contact twice is safe. If an identical request is still in progress, the API returns `200` with the *existing* `requestId` rather than creating a second request, so a retrying integration never duplicates work or spends credits twice. Poll the returned `requestId` either way.

Changing `enrichPhoneNumber` or `syncToCRM` makes it a different request, so asking for a phone number after an email-only request is always honoured.

A `402` is returned before any work starts, so retrying after a top-up is safe.

### Identifying the person

| Field                          | Description                                                                      |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `identity.linkedInUrl`         | A LinkedIn **profile** URL. Any common form is accepted                          |
| `identity.workEmail`           | Must be a company address; personal email domains are rejected                   |
| `identity.companyName`         | Identifies the employment, or the company for a name lookup                      |
| `identity.companyDomain`       | Optional hint, reduced to a registrable domain. More reliable than `companyName` |
| `identity.firstName`           | With `lastName` and a company, identifies the person by name                     |
| `identity.lastName`            | With `firstName` and a company, identifies the person by name                    |
| `identity.jobTitle`            | Optional. Helps pick the right person when several share a name                  |
| `identity.selectedCandidateId` | The person you picked from a `MULTIPLE_PEOPLE_FOUND` response                    |
| `identity.selectedCompanyId`   | The employment you picked from a `MULTIPLE_EMPLOYMENTS_FOUND` response           |

`enrichPhoneNumber` and `syncToCRM` both default to `false`, so a request that omits them enriches the email only and leaves your CRM untouched.

### Results

A resolved contact comes back with their name, LinkedIn profile, job title, employment start date and work email, plus the company they work at. A mobile number is included when you asked for one and Goodfit found it.

Contacts added this way behave like any other contact in your dataset: they're refreshed automatically, and if someone changes role or leaves, their details are updated.

A few fields are worth calling out:

* `matchedBy` says how Goodfit identified the person: `linkedInUrl`, `workEmail`, `crmRecord` or `name`. `name` means a match on the name and company you gave rather than an exact identifier, so it's worth a check.
* `inputs` echoes back your identity in the canonical form used for matching, so you can confirm what was actually looked up.
* `startDate` is the start of the employment, as `YYYY-MM-DD`.
* `isFormerEmployment` is `true` when the person has left the company you asked about. When you identify them by LinkedIn URL or work email, Goodfit still resolves them, so you can see who they were and act on the change. A name match never adds someone who has left; see `FORMER_EMPLOYEE` below.
* `addedToDataset` is `true` when **this request** added the record to your dataset, and `false` when it was already there.
* `company.addedToDataset` is `true` when the company was new to you and Goodfit added it. A newly added company appears immediately with core firmographics only; scores, segments and the rest of its data arrive with your next daily sync.

`personas` and `markets` are part of the contract but not yet populated, as persona and market matching for this API are still being built. They are empty rather than `NOT_FOUND`, so you can tell "not built yet" from "looked and found nothing".

#### Email and phone values

Every resolved contact is enriched for a work email. A mobile number is looked up only when you set `enrichPhoneNumber`. Three sentinel values tell the cases apart:

| Value                 | Description                                                               |
| --------------------- | ------------------------------------------------------------------------- |
| `NOT_FOUND`           | Searched, nothing available                                               |
| `NOT_REQUESTED`       | Not asked for (`mobilePhone` only)                                        |
| `SKIPPED_EX_EMPLOYEE` | Skipped, the resolved employment is one they have left (`workEmail` only) |

Enrichment is best effort. If a lookup fails, Goodfit still returns the resolved contact with `NOT_FOUND` in place of the value, and the request doesn't fail.

Returned emails carry the same verification statuses as every other email Goodfit supplies. See [Email and Phone enrichment](https://docs.goodfit.io/goodfit-docs/product/personas-and-contacts/email-and-phone-enrichment).

### Unresolved requests

When Goodfit can't resolve the person, `found` is `false` and `reason` gives a machine-readable reason. You aren't charged when Goodfit can't deliver a contact.

#### Reasons

| Reason                        | Description                                                                                        |
| ----------------------------- | -------------------------------------------------------------------------------------------------- |
| `PERSON_NOT_FOUND`            | No person matches the identity you gave                                                            |
| `MULTIPLE_EMPLOYMENTS_FOUND`  | The person was found but holds several roles                                                       |
| `NO_COMPANY_MATCH`            | The person was found but couldn't be tied to a company Goodfit can serve                           |
| `NOT_ENOUGH_INFORMATION`      | The identity was too thin to search on                                                             |
| `COMPANY_HAS_NO_DOMAIN`       | Their employer has no website domain                                                               |
| `COMPANY_RESOLUTION_CONFLICT` | The employer found conflicts with the CRM record you supplied                                      |
| `CONTACT_RESOLUTION_CONFLICT` | The CRM record you supplied belongs to a different person                                          |
| `CRM_RECORD_NOT_FOUND`        | The CRM record you supplied in `externalIds` isn't in your CRM                                     |
| `NAME_NOT_FOUND_AT_COMPANY`   | No one with that name was found at that company                                                    |
| `COMPANY_DOMAIN_NOT_FOUND`    | Goodfit doesn't recognise the `companyDomain` you gave                                             |
| `COMPANY_NAME_NOT_FOUND`      | Goodfit doesn't recognise the `companyName` you gave                                               |
| `MULTIPLE_PEOPLE_FOUND`       | Several people at that company fit the name                                                        |
| `FORMER_EMPLOYEE`             | Only people who have left that company fit the name. They’re listed, but not added to your dataset |

`MULTIPLE_EMPLOYMENTS_FOUND` and `MULTIPLE_PEOPLE_FOUND` are the two reasons you can act on directly. See [Handling multiple employments](https://docs.goodfit.io/goodfit-docs/apis/enrichment-api/contacts/handling-multiple-employments) and [Handling several matching people](/goodfit-docs/apis/enrichment-api/contacts/handling-several-matching-people.md).

The two conflict reasons mean your request and your CRM disagree about who or where this person is. Rather than guess, Goodfit refuses the request. It's worth resolving in the CRM before retrying.

`COMPANY_HAS_NO_DOMAIN` means Goodfit identified the person, but their employer has no website domain. This is commonly a LinkedIn placeholder page (for example "Confidential Government") or a subsidiary with no site of its own. Companies are keyed on domain in your dataset, so this one can't be added and the contact couldn't be attached to it. Retrying won't help. If the person in fact works at a company you can name, supply that `companyDomain`.

### Syncing to your CRM

Set `syncToCRM: true` and Goodfit writes the enriched contact to every connected CRM that has contact sync enabled, normally within five minutes of submission. The result carries `syncedToCRM` and, under `externalIds`, the record ID in each CRM.

How Goodfit chooses the record to write to:

* If you send an `externalIds` value Goodfit recognises, Goodfit updates that exact record.
* If you send one Goodfit hasn't seen before, Goodfit adopts that record and updates it rather than creating a second contact.
* If you send none, Goodfit looks for an existing contact on the work email, using whichever CRM field you have mapped to email in Data Points, and only creates a new record when there's no match. Goodfit searches your CRM by the mapped email field only.

Two cases are deliberately refused rather than guessed at, and appear on your Connector Logs page:

* the ID you sent points at a different record from the one Goodfit already has this person linked to;
* several contacts in your CRM share the email, so there's no single right record to update.

### Credit usage

Enrichment through this API draws on the same credits as a persona search:

* **One contact credit** per contact added to your dataset. The work email is included and isn't charged separately.
* **One phone credit** if a mobile number is found. Nothing is charged when Goodfit can't find one.

Nothing is charged when Goodfit can't deliver a contact, so an ambiguous or not-found result costs you nothing. As with persona searches, you're only charged once for each unique person at a company. Re-running the same request, or enriching someone a persona search already found, charges nothing. See [Contact Credits](https://docs.goodfit.io/goodfit-docs/product/personas-and-contacts/contact-credits).

Goodfit checks your balance before starting work, so a request you can't pay for is rejected up front rather than half-completed. Asking for a phone number requires both contact and phone credits.

A contact credit is charged for every contact delivered, **including when the person turns out to have left the company**. The result is returned with `isFormerEmployment: true` and is charged as normal.

A name match never adds someone who has left: you get `FORMER_EMPLOYEE` instead, at no charge.

### API reference

* [Enrich a contact](https://docs.goodfit.io/goodfit-docs/apis/enrichment-api/contact-endpoints#post-enrichment-enrich-contact-request)
* [Check enrichment status](https://docs.goodfit.io/goodfit-docs/apis/enrichment-api/contact-endpoints#get-enrichment-enrich-contact-request-requestid)
* [Pick a person or employment](/goodfit-docs/apis/enrichment-api/contact-endpoints.md#post-enrichment-enrich-contact-request-requestid-resolve)

For authentication, API keys, base URLs, rate limits and common API errors, see [API introduction](https://docs.goodfit.io/goodfit-docs/apis/apis-introduction).

For exact request and response schemas, field definitions and validation behaviour, refer to the API reference pages or OpenAPI specification.
