---
title: "Read the contact lookup state"
method: GET
path: "/v1/people/{id}/enrich"
tags: ["People"]
---

# Read the contact lookup state

`GET /v1/people/{id}/enrich`

## Request

- Base URL: `https://api.trayo.ai`
- URL: `https://api.trayo.ai/v1/people/{id}/enrich`
- Auth: HTTP bearer

**Required permissions:** `people:read`.

The full contact picture for a person: the email and phone to use, where each came from, and the state of the lookup behind each. This is the poll target after `POST /v1/people/{id}/enrich`: a field reads `in_progress` until its lookup finishes, then `found` or `not_found` (with `retryAvailableAt`). Free; nothing is started by reading.

## Path parameters

- `id` string, uuid, required

## Response `200`

- ContactStatusDto
  - `personId` string, uuid, required
  - `email` object, required
    - `value` string, nullable, required: The email to use: a manual value if one was written, else the found one.
    - `source` 'manual' | 'enrichment', nullable, required: `manual` = written through this API or the app; `enrichment` = found.
    - `status` 'verified' | 'unconfirmed' | 'bounced' | 'stale', nullable, required: State of a FOUND email, and the field to act on. `verified` = a finder returned it and nothing suggests it belongs to someone else. `unconfirmed` = a finder returned it, but `identityConfidence` is `conflict`: the address disagrees with this person's name or company, or another person in the workspace holds the same address. The value is still given; have a human check it before sending. `bounced` after a send bounced (the value is withheld); `stale` when the product no longer trusts it. Neither `verified` nor `unconfirmed` is a mailbox probe. Null for a manual value and when nothing was found.
    - `identityConfidence` 'match' | 'conflict' | 'unknown', nullable, required: Whether a FOUND email looks like THIS person's: `match` when its local-part agrees with their name or its domain with their account's website; `conflict` when either disagrees (a catch-all or a wrong-account hit) or when another person in the workspace holds the same address, and `status` then reads `unconfirmed`; `unknown` when there is nothing to compare against (no name, no account website, a free-mail domain, a sibling domain of the account's, or an account whose website matches none of its people's addresses). Null for a manual value and when nothing was found.
    - `foundAt` string, date-time, nullable, required
    - `lookup` object, required
      - `state` 'not_requested' | 'in_progress' | 'found' | 'not_found' | 'error' | 'bounced', required: `not_requested`: never looked up. `in_progress`: a lookup is running, poll. `found`: a value is on the record. `not_found`: the lookup ran and missed; `retryAvailableAt` says when it may run again. `error`: a run stalled and may be retried now. `bounced`: a found email bounced; the next lookup re-runs from scratch.
      - `retryAvailableAt` string, date-time, nullable, required: Set on `not_found`: the earliest time a new lookup will be accepted. Until then the field is skipped as `retry_too_soon`.
  - `phone` object, required
    - `value` string, nullable, required: The found phone number, or null.
    - `foundAt` string, date-time, nullable, required
    - `lookup` object, required
      - `state` 'not_requested' | 'in_progress' | 'found' | 'not_found' | 'error' | 'bounced', required: `not_requested`: never looked up. `in_progress`: a lookup is running, poll. `found`: a value is on the record. `not_found`: the lookup ran and missed; `retryAvailableAt` says when it may run again. `error`: a run stalled and may be retried now. `bounced`: a found email bounced; the next lookup re-runs from scratch.
      - `retryAvailableAt` string, date-time, nullable, required: Set on `not_found`: the earliest time a new lookup will be accepted. Until then the field is skipped as `retry_too_soon`.

## Other responses

- `400`: `id` is not a UUID. `validation_failed` (`retry: fix_input`) — The request did not pass input validation: a missing or malformed body field, a bad `limit`, a path parameter that is not a UUID, a `signalKey` that already exists or is reserved. `message` lists every failed constraint; `param` names the first offending field when one can be identified.
- `401`: Missing, malformed, or incorrect bearer token. Every way authentication can fail returns the identical body, so do not try to infer which one it was, and do not repeat the call with the same key. `unauthorized` (`retry: do_not_retry`) — Missing, malformed, or incorrect bearer token. Identical body for every cause.
- `403`: The key grant and its owner's current workspace role must both include every scope required by the operation. `insufficient_scope` (`retry: do_not_retry`) — Scope denial details distinguish missing_key_scope (use a key granting every missing scope), owner_permission_limit (the owner needs the missing workspace permissions), and invalid_key_scope (replace the key with supported scopes). Membership and undeclared-route denials remain generic.
- `404`: `person_not_found` (`retry: do_not_retry`) — No such person in your workspace. Another workspace's person is indistinguishable from a non-existent one.
- `429`: Every route is rate limited per workspace across REST, MCP, all keys and OAuth users, `GET /v1/whoami` included, so a balance poll competes with the work it is watching. For `rate_limited`, wait `Retry-After` seconds (also `details.retryAfterSeconds`), then send the same request once; do not retry sooner. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` describe the workspace's bucket, and they come back on successful responses too: read them and slow down BEFORE you are refused. A burst from one network can also be refused before the key is checked; that 429 carries `Retry-After` and no `RateLimit-*` headers. `rate_limited` (`retry: retry_later`) — The workspace or source network exhausted its request budget for one bucket. `details.retryAfterSeconds` (and `Retry-After` on REST) is the wait: wait that long, then retry the same request once. Do not poll or retry sooner; an early retry is refused again. If you cannot wait, stop and tell the user when to retry; do not fill the wait with other calls. `details.limit` and `details.bucket` name the exhausted budget. When present, RateLimit-* headers describe the active budget and reset.
- `500`: Something failed on our side. Nothing about the request needs changing. `internal_error` (`retry: retry_later`) — Something failed on our side. `message` never describes it — quote `requestId` when reporting. Retry with exponential backoff and GIVE UP after a few attempts: `retry_later` here does not mean the failure is known to be transient, only that nothing about your request needs changing. A request that fails this way repeatedly will not start working.

---

[API](https://skmtc.dev/trayo/apis/trayo-api.md) · [All operations](https://skmtc.dev/trayo/apis/trayo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trayo/apis/trayo-api/revisions/0af452dce6de?raw)
