> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gtm-api.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Find an email or phone number for a profile

> Run contact enrichment on one LinkedIn profile: what comes back, which of your connected accounts it runs on, and how to read a partial result.

Enrichment pulls one known person's own data from LinkedIn through a live browser
session. This page covers the two methods that can return an email or a phone number,
which of your accounts each one runs on, and how to read the receipt that comes back. A
list of other people (search results, employees, post reactions) is scraping instead:
a different surface.

## Before you start

* A connected LinkedIn account: every enrichment runs on one of your own accounts.
* The `can_act_linkedin_enrichment` permission on your API key, agent or user.
* One identifier: `profile_id` (`ACoAA...` or `ACwAA...`) **or** `public_identifier` (the
  vanity slug from `/in/...`). Exactly one, never both, or the call is
  `422 validation_failed`.

## The two methods that return contact details

|                  | `person-contact-info`                                          | `person-full-profile`                                        |
| ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------ |
| Contact fields   | `email`, `phones[]` (typed), `websites[]`, `twitter_handles[]` | flat `email`, `phone`, `twitter`, `facebook`                 |
| Everything else  | nothing else                                                   | headline, position, company, about, experience, and the rest |
| Cache TTL        | 24 hours                                                       | 24 hours                                                     |
| Where it can run | only an account of yours that is a 1st-degree connection       | your pinned account, or one the service picks for you        |

Both ride the same read but cache under their own key: a cached full profile does not
satisfy a contact-info call, and the reverse is also true.

<Warning>
  The flat `phone` on `person-full-profile` keeps only the first mobile number. Home and
  work numbers are dropped. `person-contact-info` returns `phones[]` with a `type` on each
  entry (`MOBILE`, `HOME`, `WORK`, or `null`), so use it when the phone actually matters.
</Warning>

## Run it

```bash curl theme={null}
curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-enrichment/person-contact-info" \
  -H "Authorization: Bearer gtm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"public_identifier": "williamhgates", "idempotency_key": "contact-whg-2026-08-13"}'
```

No executor fields there: contact info is routed to whichever of your accounts is
connected to the person. The call is synchronous, and the data
comes back under `result.contact_info`, next to the ledger row at
`result.data_request` (`sid` starting `er_rq_`, `status`, `executed_on`,
`served_from_cache`, `cached_from_sid`). Per-method shapes are in the
[API reference](/api-reference/overview), and batching is your own loop (no mass action,
no sequence step), one target per call.

`contact_info` is always present, and every field inside it can be empty. An empty block
means the connected account still cannot see those details, never that the call was
blocked. `address`, `birthday` and connection date are not parsed. A phone with
`type: null` is normal on older cached payloads, not a defect.

## Which account runs the call

Every enrichment runs on one of your own connected accounts.

| What you send                                | What happens                                                                                                                                                                  |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `linkedin_account_sid` (`ln_ac_` + 12 chars) | Runs on that account only, spending that account's `enrichment` daily budget. `executed_on: "own_account"`                                                                    |
| Nothing                                      | The service picks one of your connected accounts with remaining capacity and runs there. Accounts out of budget, on hold or LinkedIn rate-limited are skipped before the pick |
| `idempotency_key` (max 128 chars)            | Replay guard scoped to (team, key). A repeat returns the stored row, with no re-execution                                                                                     |

| Auto-pick situation                  | Result                                                                                       |
| ------------------------------------ | -------------------------------------------------------------------------------------------- |
| At least one account has budget left | Runs there                                                                                   |
| Accounts exist, all out of budget    | `429 rate_limited`, reason `bucket_saturated`, with `retry_after`                            |
| No connected account is ready        | `422 validation_failed`, message starting `no_connected_accounts:`. Connect an account first |

<Warning>
  There is no silent fallback for a pinned account. If the account you named is out of
  daily budget the call returns `429 rate_limited`, reason `bucket_saturated`, with a
  `retry_after` timestamp. It will not quietly move to an account you did not name.
</Warning>

### Contact info is the exception

`person-contact-info` requires a connected executor: LinkedIn reveals contact details to
1st-degree connections only, so the pick is narrowed to accounts that actually hold the
edge.

| Situation                                                          | Result                                                                                                                                                                        |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| One of your accounts holds the 1st-degree edge and has budget left | Runs there                                                                                                                                                                    |
| Your accounts hold the edge but all of them are out of budget      | `429 rate_limited`, reason `bucket_saturated`, with `retry_after` and a `connected_accounts` count                                                                            |
| Nobody on the team is connected to this person                     | `422 no_connected_account`: "none of your LinkedIn accounts is connected to this person, so this request cannot be run." Send a connection request, retry once it is accepted |
| The target has neither a member id nor a slug to match on          | `422 not_dispatchable`                                                                                                                                                        |

Here `linkedin_account_sid` is a preference, not a command: another connected account is
picked if the one you named does not qualify, and accounts out of budget, on hold or
LinkedIn rate-limited are dropped before the pick.

## How long a result is cached

| Method                                                                         | Cache TTL |
| ------------------------------------------------------------------------------ | --------- |
| `person-contact-info`, `person-full-profile`                                   | 24 hours  |
| `person-lite-profile`, `person-basic-profile`                                  | 24 hours  |
| The rest (experience, skills, education, posts, certifications, company reads) | 7 days    |
| `company-public-identifier`                                                    | 30 days   |

A repeat for the same person and method inside the TTL is served from the stored result
(`served_from_cache: true`, nothing dispatched); past it, the call executes again. Cache
is per team, per method.

## How to read a partial result

An empty answer is a real answer. A person who never published a phone number produces a
`completed` row with an empty field, not an error.

| What the receipt shows                                    | What happened                                                                                                                                 |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `served_from_cache: true`, `cached_from_sid: "er_rq_..."` | Served from your team's cached copy. Open `GET /api/data-requests/{cached_from_sid}` and read its `completed_at` for the true age of the data |
| `served_from_cache: false`                                | Ran live on the account named in `linkedin_account_sid`, spending its daily budget                                                            |

| Status      | Meaning                                              |
| ----------- | ---------------------------------------------------- |
| `running`   | Dispatched to a browser, visible mid-flight          |
| `completed` | Result stored, whether executed or served from cache |
| `rejected`  | Refused before dispatch                              |
| `failed`    | Dispatched and terminally failed                     |

A row is inserted either `running` or already terminal; `running` then moves to
`completed`, `failed` or `rejected` (a reservation failure rejects an already running
row).

A call has a hard deadline of 90 seconds; typical latency is 30 seconds at p95 executed,
500 ms at p95 cache-served. Retrying is safe when you reuse the `idempotency_key`; inside
that window the replay returns the `running` row, so poll `GET /api/data-requests/{sid}`
until the status is terminal. To follow results without polling, subscribe to the
`data-requests.completed` and `data-requests.failed` webhooks; the completed payload adds
`result_ref` and `completed_at` to the receipt fields. Refusals have no
event: a rejection appears only as the synchronous HTTP error.

## Daily budget

Enrichment spends one shared bucket per account called `enrichment`. Read it before a
batch, on the Smart Limits tab of the account drawer or through
[smart limits and warmup](/kb/smart-limits-and-warmup) filtered on `limit_type`.

<Warning>
  Default budget: 80 enrichment actions per day per account, with a 240 second gap enforced
  between counted actions. When the budget is spent or the gap has not elapsed the call
  returns `429 rate_limited`, reason `bucket_saturated`, with a `retry_after` value; retry
  after that timestamp, not on a fixed schedule. The free plan clamps this to 12 enrichments
  per day per connected account.
</Warning>

## Errors you will actually hit

| Scenario                                                        | HTTP | Code                                               | What to do                                                                                                     |
| --------------------------------------------------------------- | ---- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Slug-only call on a method that needs a stored profile URN      | 422  | `not_dispatchable`                                 | Run `person-lite-profile` for that slug first: it stores the URN, and the sub-record methods work from then on |
| A transient browser or plugin failure                           | 503  | `infrastructure_unavailable`, `plugin_unavailable` | Back off and retry the same call; `plugin_unavailable` carries a `retry_after` one minute out                  |
| Terminal execution failure                                      | 409  | `operation_blocked`                                | Do not retry as sent, change the input                                                                         |
| Method not implemented yet (`person-languages`, `post-details`) | 501  | `not_implemented`                                  | Nothing is ledgered: no row, no budget spent                                                                   |

`bucket_saturated` on a 429 is your own account's daily enrichment budget, nothing wider.
Wait for `retry_after`, or pin a different connected account that still has budget.

<Tip>
  Copy this to your AI assistant to enrich a list of profiles:

  "Enrich LinkedIn profile slugs with contact details using the GTM API at
  [https://app.gtm-api.com/linkedin/v4](https://app.gtm-api.com/linkedin/v4). For each slug POST to
  /api/linkedin-enrichment/person-contact-info with body `{"public_identifier": SLUG,
    "idempotency_key": "contact-SLUG"}` and header Authorization: Bearer gtm\_live\_YOUR\_KEY. Do
  not send linkedin\_account\_sid: this method picks my connected
  account on its own. One profile per call, sequentially. Treat 422 no\_connected\_account as
  skip-and-log, and on 429 wait until the retry\_after timestamp. Write CSV: slug, email,
  first phone, phone type, websites, data\_request.status."
</Tip>

## Related

* [Smart limits and warmup](/kb/smart-limits-and-warmup)
* [Billing and plans](/kb/billing-and-plans)
