Polling, zero setup

7 min read

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

All live and contact endpoints run in the background. They reply immediately with a webhookId, then deliver the actual result once it’s ready, usually within seconds.

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
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.result
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, 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 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
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
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
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
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
{
  "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
{
  "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. 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 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 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.

Previous

Rate limits & credits

Next

Webhooks