Integration guides
Polling, zero setup
UpdatedThe 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 works the same way.
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.
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
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));
}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.resultimport 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)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 deliveryPolling 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 at the end of this guide.
How it works
- Create a job. Call any async endpoint (e.g.
POST /v2/fetch/persons/live). You can omitwebhookUrlfrom the request body when you intend to retrieve the result by polling. - Receive the webhookId. The API responds immediately with
{ status: "created", webhookId, pollUrl }. - Poll for the result. Call
GET /v2/webhooks/:webhookIdperiodically untilstatusis"succeeded"or"errored". - Read the data. When
statusis"succeeded", theresultfield contains the enrichment payload.
Step 1: Trigger a job
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)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"
}'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
{
"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).
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
}
}curl "https://api.reversecontact.com/v2/webhooks/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
-H "Authorization: Bearer YOUR_API_KEY"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
{
"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
{
"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
{
"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.
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:
- Open Logs › Job Lookup from the sidebar.
- Paste any
webhookIdreturned by an async endpoint. - 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 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.
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.