# Find Email

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

## Introduction

The Find Email endpoint discovers professional email addresses 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 an email address 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]
> **Every email we return is verified.** Each candidate address goes through our own validation process, then through a waterfall of professional verification providers chosen among the best on the market: we only return addresses that clear both. We monitor these providers regularly to select the best one when needed, and because the market evolves quickly, we continuously evaluate new vendors to reinforce this waterfall.

## 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 emails 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": "jane.doe@acme.com",
      "type": "professional"
    }
  ]
}
```

**data** (FoundEmail): One entry per email address discovered for the person.

| Field   | Type   | Description                              |
| ------- | ------ | ---------------------------------------- |
| `value` | string | The discovered email address             |
| `type`  | string | Email category, typically `professional` |

## Webhook callback errors (advanced)

When the lookup cannot return an email, `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": "email-not-found"
  }
}
```

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

| Code                      | Billed | Description                                                       |
| ------------------------- | ------ | ----------------------------------------------------------------- |
| `email-not-found`         | No     | No email address 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 **3 credits only when an email is found**. A lookup that returns `email-not-found`, `person-not-found`, `invalid-request`, or `fetch-data-error` costs 0 credits. A delivery failure remains billed when the email 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/email", {
  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/email", {
  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/email \
  -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/email \
  -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/email",
    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/email",
    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_e1m2a3i4l5f6",
    "executionTimeMs": 95
  },
  "quotas": {
    "creditsConsumed": 3,
    "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": "jane.doe@acme.com",
      "type": "professional"
    }
  ]
}
```
