> ## 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 |
| The account's browser could not serve the call this time | 503 | `browser_starting`, `browser_unavailable`, `plugin_unavailable`, `browser_maintenance`, `infrastructure_maintenance` | Nothing is wrong with the input. Wait for `retry_after` and send the call again: 30 seconds while a browser is still starting, one to two minutes after a lost session or a page that did not load, five to ten minutes while the browser or the fleet is being updated. A repeat under the same `idempotency_key` returns the stored failed row, so retry with a new key |
| 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)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.