# Find Phone

> Find a professional phone number from a LinkedIn URL or full name and company domain, then receive the result by webhook.

## Introduction

The Find Phone endpoint discovers professional phone numbers for a person. You can identify the person with a LinkedIn profile URL, or with their full name and company domain. Processing is asynchronous: the API acknowledges the request with a `webhookId`, then you retrieve the result by [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or through a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery).

Use this endpoint when you need a phone number for outreach or CRM enrichment. For push delivery, configure a workspace webhook URL in **Settings > Webhooks**, or provide `webhookUrl` in the request.

> [!NOTE]
> **Async, not real-time.** We aim to deliver each result in under 15 seconds, but it can take tens of minutes depending on conditions. Built to update databases and feed async pipelines, not to back a synchronous request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) for delivery details.

> [!NOTE]
> **Lower coverage than email, billed only when found.** Phone numbers are harder to source than email addresses, so expect fewer matches than with the [Find Email endpoint](https://docs.reversecontact.com/endpoints/contact-email). A lookup is billed **20 credits only when at least one phone number is found**; a lookup that finds nothing costs 0 credits.

## Authorization

| Name     | Type   | Required | Description                                                                                         |
| -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- |
| `apikey` | string | Yes      | Your API key from the developer dashboard. Pass it in the `Authorization: Bearer YOUR_API_KEY` header. |

## Request body

Choose one lookup strategy. Use either a LinkedIn profile URL, or a full name with a company domain. The split `firstName` + `lastName` form is also accepted.

| Name            | Type   | Required    | Description                                                                                              |
| --------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------- |
| `url`           | string | Conditional | Public LinkedIn profile URL, such as `https://www.linkedin.com/in/janedoe`. Use this for the URL strategy. |
| `fullName`      | string | Conditional | Person's full name, such as `Jane Doe`. Use this with `companyDomain`.                                   |
| `firstName`     | string | Conditional | Person's first name. Use this with `lastName` and `companyDomain` instead of `fullName`.                 |
| `lastName`      | string | Conditional | Person's last name. Use this with `firstName` and `companyDomain` instead of `fullName`.                 |
| `companyDomain` | string | Conditional | Company domain, such as `acme.com`. Required for the name-based strategies.                               |
| `webhookUrl`    | string | Optional    | HTTPS URL that receives the callback. Omit it to retrieve the result by polling instead.                |

> [!NOTE]
> The request must contain `url`, or `fullName` + `companyDomain`, or `firstName` + `lastName` + `companyDomain`.

## Response structure

This endpoint replies immediately with a `webhookId`. You then have two ways to retrieve the actual result:

1. **Polling (recommended).** Call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. No webhook URL needed. See the [Polling guide](https://docs.reversecontact.com/guides/polling).
2. **Webhook callback (advanced).** If you provided a `webhookUrl` (or a workspace default is configured), the gateway POSTs the phone numbers to that URL once the lookup completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below.

| Field     | Type    | Description                                |
| --------- | ------- | ------------------------------------------ |
| `success` | boolean | `true` if the job was created successfully |

**data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses).

| Field       | Type   | Description                                                                                                                            |
| ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `status`    | string | Job status, always `"created"` for the initial response                                                                                |
| `webhookId` | string | Unique identifier (UUID). **Save it.** Use it to poll the result at `GET /v2/webhooks/:webhookId`, or to correlate an incoming webhook callback. |
| `pollUrl`   | string | Convenience hint pointing to the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`).                                      |

| Field   | Type   | Description              |
| ------- | ------ | ------------------------ |
| `error` | `null` | Always `null` on success |

**quotas** (V2QuotaInfo): Credits and rate limit usage after this request

| Field             | Type   | Description                      |
| ----------------- | ------ | -------------------------------- |
| `creditsConsumed` | number | Credits consumed by this request |

**workspace** (Workspace): Workspace-level limits

| Field                 | Type    | Description                                    |
| --------------------- | ------- | ---------------------------------------------- |
| `id`                  | string  | Your workspace identifier                      |
| `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited        |
| `allowDailyOvercost`  | boolean | Whether the workspace may exceed its daily cap |

**credits** (Credits): Credit balance

| Field   | Type   | Description                |
| ------- | ------ | -------------------------- |
| `total` | number | Total credits in your plan |
| `used`  | number | Credits consumed so far    |
| `left`  | number | Remaining credits          |

**dailyLimit** (DailyLimit): Daily request limit (`null` if not configured)

| Field       | Type   | Description                             |
| ----------- | ------ | --------------------------------------- |
| `limit`     | number | Maximum requests per day                |
| `used`      | number | Requests made today                     |
| `left`      | number | Remaining requests today                |
| `nextReset` | string | When the daily window resets (ISO 8601) |

**minuteRateLimit** (MinuteRateLimit): Per-minute rate limit

| Field       | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| `limit`     | number | Maximum requests per minute              |
| `used`      | number | Requests made in the current minute      |
| `left`      | number | Remaining requests in the current minute |
| `nextReset` | string | When the minute window resets (ISO 8601) |

**key** (ApiKey): API key-level limits (`null` when request is from dashboard)

| Field | Type   | Description        |
| ----- | ------ | ------------------ |
| `id`  | string | API key identifier |

**dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured)

| Field       | Type   | Description                             |
| ----------- | ------ | --------------------------------------- |
| `limit`     | number | Maximum requests per day                |
| `used`      | number | Requests made today                     |
| `left`      | number | Remaining requests today                |
| `nextReset` | string | When the daily window resets (ISO 8601) |

**minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key

| Field       | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| `limit`     | number | Maximum requests per minute              |
| `used`      | number | Requests made in the current minute      |
| `left`      | number | Remaining requests in the current minute |
| `nextReset` | string | When the minute window resets (ISO 8601) |

**metadata** (V2ResponseMetadata): Request tracking information

| Field             | Type   | Description                          |
| ----------------- | ------ | ------------------------------------ |
| `requestId`       | string | Unique identifier for this request   |
| `executionTimeMs` | number | Total execution time in milliseconds |

## Webhook callback payload (advanced)

When the lookup succeeds, ReverseContact sends a JSON `POST` request to your `webhookUrl`. The callback is not wrapped in the V2 response envelope. It always includes the `webhookId` from the initial acknowledgement and a `data` value.

**Success payload**

```json [Webhook callback (success)]
{
  "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4",
  "data": [
    {
      "value": "+33612345678",
      "type": "mobile"
    }
  ]
}
```

**data** (FoundPhone): One entry per phone number discovered for the person.

| Field   | Type           | Description                                                                                   |
| ------- | -------------- | --------------------------------------------------------------------------------------------- |
| `value` | string         | The discovered phone number, in the format reported by the provider (usually E.164)           |
| `type`  | string \| null | Phone kind reported by the provider, such as `mobile`; `null` when the provider gives no kind |

## Webhook callback errors (advanced)

When the lookup cannot return a phone number, `data` is **not** an array. Instead, it contains an `errorCode` identifying the failure reason:

**Error payload**

```json [Webhook callback (error)]
{
  "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4",
  "data": {
    "errorCode": "phone-not-found"
  }
}
```

`data.errorCode` will be one of the following values:

| Code                      | Billed | Description                                                       |
| ------------------------- | ------ | ----------------------------------------------------------------- |
| `phone-not-found`         | No     | No phone number was discovered for the person                     |
| `person-not-found`        | No     | No matching person was found for the supplied identifiers         |
| `invalid-request`         | No     | The lookup input is malformed or does not match an accepted form  |
| `fetch-data-error`        | No     | The lookup failed while fetching data; safe to retry              |
| `webhook-url-invalid`     | Yes    | The callback URL returned a 4xx response                          |
| `webhook-url-errored`     | Yes    | The callback URL returned a 5xx response or a network error       |
| `webhook-url-timeout`     | Yes    | The callback URL did not respond in time                          |
| `webhook-url-unreachable` | Yes    | The callback URL could not be reached                             |

> [!NOTE]
> The lookup costs **20 credits only when at least one phone number is found**. A lookup that returns `phone-not-found`, `person-not-found`, `invalid-request`, or `fetch-data-error` costs 0 credits. A delivery failure remains billed when a phone number was successfully found.

## Code examples

**Code Examples**

```javascript [JavaScript - Polling (recommended)]
const KEY = "YOUR_API_KEY";
const BASE = "https://api.reversecontact.com";

const trigger = await fetch("https://api.reversecontact.com/v2/contact/phone", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    url: "https://www.linkedin.com/in/janedoe"
  })
});
const { data: { webhookId } } = await trigger.json();

async function pollUntilDone(id) {
  while (true) {
    const res = await fetch(`${BASE}/v2/webhooks/${id}`, {
      headers: { "Authorization": `Bearer ${KEY}` }
    });
    const { data } = await res.json();
    if (data.status === "succeeded") return data.result;
    if (data.status === "errored")  throw new Error(data.errorCode ?? "Job failed");
    await new Promise(r => setTimeout(r, 2000));
  }
}

const result = await pollUntilDone(webhookId);
console.log(result);
```
```javascript [JavaScript - Webhook]
const response = await fetch("https://api.reversecontact.com/v2/contact/phone", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    fullName: "Jane Doe",
    companyDomain: "acme.com",
    webhookUrl: "https://your-app.com/webhooks/reversecontact"
  })
});

const result = await response.json();
console.log(result.data.status, result.data.webhookId);
```
```bash [cURL - Polling (recommended)]
KEY="YOUR_API_KEY"
ID=$(curl -sX POST https://api.reversecontact.com/v2/contact/phone \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://www.linkedin.com/in/janedoe"}' | jq -r .data.webhookId)

while :; do
  S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \
    -H "Authorization: Bearer $KEY" | jq -r .data.status)
  [ "$S" = "succeeded" ] && break
  [ "$S" = "errored" ] && echo "job failed" && exit 1
  sleep 2
done

curl -s https://api.reversecontact.com/v2/webhooks/$ID \
  -H "Authorization: Bearer $KEY" | jq .data.result
```
```bash [cURL - Webhook]
curl -X POST https://api.reversecontact.com/v2/contact/phone \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fullName": "Jane Doe",
    "companyDomain": "acme.com",
    "webhookUrl": "https://your-app.com/webhooks/reversecontact"
  }'
```
```python [Python - Polling (recommended)]
import time
import requests

KEY = "YOUR_API_KEY"
BASE = "https://api.reversecontact.com"

trigger = requests.post(
    "https://api.reversecontact.com/v2/contact/phone",
    headers={
        "Authorization": f"Bearer {KEY}",
        "Content-Type": "application/json"
    },
    json={
        "url": "https://www.linkedin.com/in/janedoe"
    }
)
webhook_id = trigger.json()["data"]["webhookId"]

def poll_until_done(wid):
    while True:
        res = requests.get(
            f"{BASE}/v2/webhooks/{wid}",
            headers={"Authorization": f"Bearer {KEY}"}
        )
        data = res.json()["data"]
        if data["status"] == "succeeded":
            return data["result"]
        if data["status"] == "errored":
            raise RuntimeError(data.get("errorCode") or "Job failed")
        time.sleep(2)

result = poll_until_done(webhook_id)
print(result)
```
```python [Python - Webhook]
import requests

response = requests.post(
    "https://api.reversecontact.com/v2/contact/phone",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
        "fullName": "Jane Doe",
        "companyDomain": "acme.com",
        "webhookUrl": "https://your-app.com/webhooks/reversecontact"
    }
)

result = response.json()
print(result["data"]["status"], result["data"]["webhookId"])
```

## Example response

**Example Response**

```json [Initial response]
{
  "success": true,
  "data": {
    "status": "created",
    "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4",
    "pollUrl": "/v2/webhooks/b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4"
  },
  "error": null,
  "metadata": {
    "requestId": "req_p1h2o3n4e5f6",
    "executionTimeMs": 95
  },
  "quotas": {
    "creditsConsumed": 20,
    "workspace": {
      "id": "ws_abc123",
      "credits": {
        "total": 10000,
        "used": 4539,
        "left": 5461
      },
      "dailyLimit": null,
      "minuteRateLimit": {
        "limit": 60,
        "used": 10,
        "left": 50,
        "nextReset": "2025-01-20T14:16:00.000Z"
      },
      "hasUnlimitedCredits": false,
      "allowDailyOvercost": false
    },
    "key": {
      "id": "key_abc123",
      "dailyLimit": null,
      "minuteRateLimit": {
        "limit": 60,
        "used": 10,
        "left": 50,
        "nextReset": "2025-01-20T14:16:00.000Z"
      }
    }
  }
}
```
```json [Webhook callback]
{
  "webhookId": "b8c9d0e1-f2a3-4b4c-5d6e-7f8091a2b3c4",
  "data": [
    {
      "value": "+33612345678",
      "type": "mobile"
    }
  ]
}
```
