# Polling, zero setup

> The recommended way to retrieve async enrichment results. Trigger a job, pull the result on demand, free of charge, no public HTTPS endpoint required.

## The recommended way to retrieve async results

All `live` endpoints run in the background. They reply immediately with a `webhookId`, then deliver the actual result once it's ready, usually within seconds. The [Contact Email Finder](https://docs.reversecontact.com/endpoints/contact-email) works the same way.

> [!IMPORTANT]
> **Async by design, no synchronous SLA.** We aim to deliver each result in under 15 seconds, but processing can stretch to tens of minutes depending on conditions (source availability, queue load, retries). Even with fast polling, live endpoints are not built to sit inline in a synchronous, real-time request a user is waiting on. Use them to update databases, enrich records, and feed async pipelines. Polling and webhooks both keep the work off your critical path.

**Polling is the recommended way to retrieve that result**: trigger a job, then pull the result on demand by calling `GET /v2/webhooks/:webhookId` until it's ready.

It works without any infrastructure on your side: no public URL to expose, no receiver to deploy, no tunnel to maintain. The same delivery model fits backend services, batch pipelines, mobile apps, CLI tools, notebooks, and local development.

> [!TIP]
> **Recommended for most teams.**
>
> - **No public HTTPS endpoint required.** Nothing to deploy, no tunnel to maintain.
> - **Free.** Status checks cost 0 credits, whatever the status returned.
> - **Off the rate-limit clock.** Polling does not count toward your per-minute RPM.
> - **Works everywhere.** Backend, mobile, CLI, notebook, behind a firewall.

## Trigger and poll

Trigger any async endpoint, save the `webhookId` it returns, then call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. That's the entire integration.

**Trigger and poll**

```javascript [JavaScript]
const KEY = "YOUR_API_KEY";
const BASE = "https://api.reversecontact.com";

const trigger = await fetch(`${BASE}/v2/fetch/persons/live`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ url: "https://social.com/in/janedoe" })
});
const { data: { webhookId } } = await trigger.json();

while (true) {
  const res = await fetch(`${BASE}/v2/webhooks/${webhookId}`, {
    headers: { "Authorization": `Bearer ${KEY}` }
  });
  const { data } = await res.json();
  if (data.status === "succeeded") { console.log(data.result); break; }
  if (data.status === "errored")   throw new Error(data.errorCode ?? "Job failed");
  await new Promise(r => setTimeout(r, 2000));
}
```
```bash [cURL]
KEY="YOUR_API_KEY"
ID=$(curl -sX POST https://api.reversecontact.com/v2/fetch/persons/live \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://social.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
```
```python [Python]
import time, requests

KEY  = "YOUR_API_KEY"
BASE = "https://api.reversecontact.com"
H    = {"Authorization": f"Bearer {KEY}"}

trigger = requests.post(
    f"{BASE}/v2/fetch/persons/live",
    headers={**H, "Content-Type": "application/json"},
    json={"url": "https://social.com/in/janedoe"}
)
webhook_id = trigger.json()["data"]["webhookId"]

while True:
    data = requests.get(f"{BASE}/v2/webhooks/{webhook_id}", headers=H).json()["data"]
    if data["status"] == "succeeded":
        print(data["result"])
        break
    if data["status"] == "errored":
        raise RuntimeError(data.get("errorCode") or "Job failed")
    time.sleep(2)
```

> [!TIP]
> **No code yet? Try it from the dashboard.** Trigger any async endpoint, copy the `webhookId`, paste it in **[Logs > Job Lookup](https://app.reversecontact.com/logs/jobs)**, and watch the status update in real time. Same API under the hood, zero integration required.

## Which delivery method should you use?

```
Do you already run a public HTTPS receiver?
   |
   +-- NO   -> Polling (recommended)
   |
   +-- YES  -> Polling (recommended), or Webhooks if you prefer push delivery
```

Polling works at any volume and is the simplest path for every team. Webhooks are an alternative for teams that already operate a public HTTPS receiver and prefer a push model.

## Polling vs webhooks

Both handle the same volumes, the same data, and the same enrichment cost. The only real difference is the delivery model (pull vs push) and the trade-offs that come with it.

| Criterion | Polling (recommended) | Webhooks (advanced) |
| --------- | --------------------- | ------------------- |
| **Setup time** | under 1 min | 30 to 60 min |
| Public HTTPS endpoint | not required | required |
| Local dev, mobile, CLI | works out of the box | needs ngrok or a tunnel |
| Cost per status check | 0 credits | 0 credits |
| Volume capacity | unlimited | unlimited |

Same endpoints, same data, same enrichment cost. Only the delivery mechanism differs.

## When to use polling

Polling fits the vast majority of integrations:

- **Backend services and APIs.** Server-to-server calls where you want the simplest possible delivery model, with one less moving part to monitor.
- **Batch jobs and pipelines.** A pipeline step that needs to block on the enrichment result as if the call were synchronous, without standing up a callback receiver.
- **Internal tools behind a firewall or VPN.** Admin consoles or back-office systems with no public surface.
- **Mobile, desktop, CLI, notebooks.** Anything without a backend to receive callbacks.
- **Compliance-constrained environments.** Security posture that disallows exposing inbound webhook receivers, or restricts which services can receive third-party callbacks.
- **Local development and prototyping.** Skip ngrok / Cloudflare Tunnel entirely while you're iterating.

Webhooks are an alternative when you already operate a public HTTPS receiver and prefer a push model. For everything else, polling is faster to integrate and equally cost-effective at any volume.

Polling and webhook callbacks are not mutually exclusive. You can use both, e.g. webhooks as the primary delivery channel and polling as a fallback for status checks. See [Combining polling with a webhookUrl](#combining-polling-with-a-webhookurl) at the end of this guide.

## How it works

1. **Create a job.** Call any async endpoint (e.g. `POST /v2/fetch/persons/live`). You can omit `webhookUrl` from the request body when you intend to retrieve the result by polling.
2. **Receive the webhookId.** The API responds immediately with `{ status: "created", webhookId, pollUrl }`.
3. **Poll for the result.** Call `GET /v2/webhooks/:webhookId` periodically until `status` is `"succeeded"` or `"errored"`.
4. **Read the data.** When `status` is `"succeeded"`, the `result` field contains the enrichment payload.

## Step 1: Trigger a job

```javascript [JavaScript]
const res = await fetch("https://api.reversecontact.com/v2/fetch/persons/live", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    url: "https://linkedin.com/in/williamhgates"
  })
})
const { data } = await res.json()
console.log(data.webhookId, data.pollUrl)
```
```bash [cURL]
curl -X POST "https://api.reversecontact.com/v2/fetch/persons/live" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://linkedin.com/in/williamhgates"
  }'
```
```python [Python]
import requests

res = requests.post(
    "https://api.reversecontact.com/v2/fetch/persons/live",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={"url": "https://linkedin.com/in/williamhgates"}
)
data = res.json()["data"]
print(data["webhookId"], data["pollUrl"])
```

The response includes a `pollUrl` hint pointing to the polling endpoint:

**Initial Response**

```json [JSON]
{
  "success": true,
  "data": {
    "status": "created",
    "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "pollUrl": "/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
  },
  "error": null
}
```

## Step 2: Poll for the result

Call `GET /v2/webhooks/:webhookId` until you receive a terminal status (`succeeded` or `errored`).

```javascript [JavaScript]
async function pollUntilDone(webhookId) {
  while (true) {
    const res = await fetch(`https://api.reversecontact.com/v2/webhooks/${webhookId}`, {
      headers: { "Authorization": "Bearer YOUR_API_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)) // wait 2s before next poll
  }
}
```
```bash [cURL]
curl "https://api.reversecontact.com/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
  -H "Authorization: Bearer YOUR_API_KEY"
```
```python [Python]
import time
import requests

def poll_until_done(webhook_id):
    while True:
        res = requests.get(
            f"https://api.reversecontact.com/v2/webhooks/{webhook_id}",
            headers={"Authorization": "Bearer YOUR_API_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)
```

### Response shapes

While the job is still running:

**in_progress**

```json [JSON]
{
  "success": true,
  "data": {
    "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "status": "in_progress",
    "eventType": "FETCH_PERSON_DATA",
    "errorCode": null,
    "createdAt": "2026-04-08T12:43:02.017Z",
    "result": null,
    "creditsConsumed": 0
  }
}
```

When the job completes successfully:

**succeeded**

```json [JSON]
{
  "success": true,
  "data": {
    "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "status": "succeeded",
    "eventType": "FETCH_PERSON_DATA",
    "errorCode": null,
    "createdAt": "2026-04-08T12:43:02.017Z",
    "result": {
      "firstName": "Bill",
      "lastName": "Gates",
      "headline": "Chair, Gates Foundation and Founder, Breakthrough Energy",
      "...": "endpoint-specific result payload"
    },
    "creditsConsumed": 2
  }
}
```

When the job fails:

**errored**

```json [JSON]
{
  "success": true,
  "data": {
    "webhookId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "status": "errored",
    "eventType": "FETCH_PERSON_DATA",
    "errorCode": "person-not-found",
    "createdAt": "2026-04-08T12:43:02.017Z",
    "result": null,
    "creditsConsumed": 0
  }
}
```

`creditsConsumed`: credits billed for this job so far. 0 while the job is in progress; the final amount once it has completed.

> [!NOTE]
> If the enrichment was fetched but the push to your `webhookUrl` failed, the job still resolves as `status: "succeeded"` with the `result` populated, plus a `deliveryStatus` field naming the delivery problem (`webhook-url-invalid`, `webhook-url-errored`, `webhook-url-timeout`, or `webhook-url-unreachable`). Poll as usual to retrieve the data. These delivery failures are billed (the data was fetched) and not refunded; see [Rate limits & credits](https://docs.reversecontact.com/guides/rate-limits-credits). `errorCode` is reserved for genuine enrichment failures, so it stays `null` in this case.

## Polling frequency

The polling endpoint is **free** (zero credits) and **does not count against your per-minute rate limit**. That said, please poll responsibly:

| Use case | Recommended interval |
| -------- | -------------------- |
| Interactive UI (a user is waiting) | every 2 seconds |
| Background job (batch processing) | every 5 to 10 seconds |
| Long-running enrichment | every 30 seconds with exponential backoff |

Most jobs complete within a few seconds. Polling more aggressively than once per second offers no real benefit and increases load on your application.

## Inspect and retrieve results from the dashboard

If you don't want to write code yet, or you just need to check a single job, you can use **Job Lookup** directly from the dashboard:

1. Open [Logs › Job Lookup](https://app.reversecontact.com/logs/jobs) from the sidebar.
2. Paste any `webhookId` returned by an async endpoint.
3. Watch the job status update in real time, inspect the full result payload, and download it as JSON.

You can also jump straight to Job Lookup from the **[Webhook Events](https://app.reversecontact.com/logs/events)** page. Every row has an action button on the right that opens the lookup pre-filled for that event.

This is the fastest way to test the polling experience end-to-end without writing a single line of integration code, and a convenient tool for support, ops, and manual debugging sessions. It uses the exact same API under the hood, so what you see in the dashboard is exactly what your code will receive from `GET /v2/webhooks/:webhookId`.

## Result retention

Polling results are retained for **72 hours by default**, counted from the moment the job is **created** (not from when it completes). After that window, the record and its stored payload are purged and the polling endpoint returns a `WEBHOOK_EVENT_EXPIRED` error.

> [!IMPORTANT]
> The 72-hour countdown starts at job creation, so for unusually long-running enrichments, fetch the result shortly after completion rather than days later. Workspaces with longer retention requirements can reach out to support to extend this window.

## Error responses

| Status | Code | When |
| ------ | ---- | ---- |
| `404` | `WEBHOOK_EVENT_NOT_FOUND` | The `webhookId` doesn't exist or doesn't belong to your workspace |
| `410` | `WEBHOOK_EVENT_EXPIRED` | The result has expired and is no longer retrievable |

## Combining polling with a webhookUrl

If you provide a `webhookUrl` in the request body, the API will deliver the result to that URL **and** the same result remains retrievable via polling. This is useful when:

- You want webhooks as the primary path but need a fallback mechanism in case a delivery is missed.
- You want to inspect or re-fetch a result after the original webhook delivery.
- You're migrating from webhooks to polling and want to validate parity before switching.
