Skip to content

MaintenanceThe former company search endpoint goes into maintenance on November 9Migration guide

Learn // Async results

Polling for async results

Updated

Last verified

The recommended way to retrieve async enrichment results: trigger a job, then poll for the result. Polling is free and needs no public HTTPS endpoint.

All live endpoints run in the background. They reply immediately with a webhookId, then deliver the result once it's ready, usually within seconds. Find Email, Verify Email and Find Phone work the same way.

Polling is the recommended way to retrieve that result: trigger a job, then call GET /v2/webhooks/:webhookId until the result is ready.

It needs no infrastructure on your side: no public URL to expose, no receiver to deploy, no tunnel to maintain. It 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?wait=20 until the status is succeeded. Each call answers as soon as the result is ready, or after at most 20 seconds with the job still running. The loop starts two polls at least Retry-After seconds apart, so a call that comes back early is followed by a short pause (see Polling frequency).

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 started = Date.now();
  // Answers as soon as the job ends, or after at most 20 seconds with the job still running.
  const res = await fetch(`${BASE}/v2/webhooks/${webhookId}?wait=20`, {
    headers: { "Authorization": `Bearer ${KEY}` },
    signal: AbortSignal.timeout(30000) // longer than the wait
  });
  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");
  // Still running: start the next poll at least Retry-After seconds after this one.
  const retryAfter = Number(res.headers.get("Retry-After")) || 2;
  await new Promise(r => setTimeout(r, retryAfter * 1000 - (Date.now() - started)));
}

Which delivery method should you use?

Text
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 takes the least setup. Webhooks are an alternative for teams that already operate a public HTTPS receiver and prefer a push model.

Polling vs webhooks

Polling and webhooks use the same endpoints, handle the same volumes, and return the same data at the same enrichment cost. They differ in the delivery model: with polling you pull the result, with webhooks we push it to your server.

CriterionPolling (recommended)Webhooks (advanced)
Setup timeunder 1 min30 to 60 min
Public HTTPS endpointnot requiredrequired
Local dev, mobile, CLIworks out of the boxneeds ngrok or a tunnel
Cost per status check0 credits0 credits
Volume capacityunlimitedunlimited

When to use polling

Polling fits most integrations:

  • Backend services and APIs: server-to-server calls where you want the simplest delivery model, with one less moving part to monitor.
  • Batch jobs and pipelines: a pipeline step that blocks on the enrichment result as if the call were synchronous, without a callback receiver.
  • Internal tools behind a firewall or VPN: admin consoles or back-office systems with no public surface.
  • Mobile, desktop, CLI and notebooks: anything without a backend to receive callbacks.
  • Compliance-constrained environments: a security policy that forbids exposing inbound webhook receivers, or restricts which services can receive third-party callbacks.
  • Local development and prototyping: no ngrok or Cloudflare Tunnel to run while you iterate.

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.

You can also combine the two, for example webhooks as the primary delivery channel and polling as a fallback for status checks. See Combining polling with a webhookUrl.

How it works

  1. Call any async endpoint (for example POST /v2/fetch/persons/live) to create a job. You can omit webhookUrl from the request body when you intend to retrieve the result by polling.
  2. The API responds immediately with { status: "created", webhookId, pollUrl }.
  3. Call GET /v2/webhooks/:webhookId?wait=20 until status is "succeeded" or "errored". Each call waits up to 20 seconds for the job to end; start two polls at least Retry-After seconds apart.
  4. When status is "succeeded", the result field 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://social.com/in/janedoe"
  })
})
const { data } = await res.json()
console.log(data.webhookId, data.pollUrl)

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

JSON

Initial Response

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

It also carries a Retry-After header: the number of seconds to wait before the first poll. A poll with ?wait=20 can start right away, since it waits for the job itself.

Step 2: Poll for the result

Call GET /v2/webhooks/:webhookId?wait=20 until you receive a terminal status (succeeded or errored). The call answers as soon as the job ends, or after at most 20 seconds with the job still running: check the status, and start the next poll at least Retry-After seconds after the previous one started.

async function pollUntilDone(webhookId) {
  while (true) {
    const started = Date.now()
    const res = await fetch(`https://api.reversecontact.com/v2/webhooks/${webhookId}?wait=20`, {
      headers: { "Authorization": "Bearer YOUR_API_KEY" },
      signal: AbortSignal.timeout(30000) // longer than the wait
    })
    const { data } = await res.json()
    if (data.status === "succeeded") return data.result
    if (data.status === "errored")  throw new Error(data.errorCode ?? "Job failed")
    // Still running: a held poll lasted longer than Retry-After, an early answer pauses for the rest.
    const retryAfter = Number(res.headers.get("Retry-After")) || 2
    await new Promise(r => setTimeout(r, retryAfter * 1000 - (Date.now() - started)))
  }
}

Response shapes

While the job is still running (this response also carries a Retry-After header, see Polling frequency):

JSON

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:

JSON

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:

JSON

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, with or without wait. Most jobs complete within a few seconds.

One pacing rule covers every loop: start two polls at least Retry-After seconds apart, and 2 seconds apart when the header is missing. Every answer with the job still running carries a Retry-After header (Retry-After: 2), early answers to a long poll included. The response that creates the job carries it too, for the first poll. The header is absent once the job has succeeded or errored. The rule paces answers with the job still running: a 5xx is retried as Error handling & retries describes.

ApproachHow the rule appliesWhen the result reaches you
Long polling (recommended)GET /v2/webhooks/:webhookId?wait=20. A held poll already lasts longer than Retry-After, so the next one starts right away; after an early answer, pause for the rest of Retry-AfterAs soon as it's ready
Polling without waitGET /v2/webhooks/:webhookId answers at once, so pause Retry-After seconds between pollsWithin Retry-After seconds

Polling more slowly than that delays each result by as much: a loop that polls every 20 seconds can receive a result up to 20 seconds after it was ready. Polling faster than Retry-After offers no benefit.

Long polling with wait

Add wait to the query string, and the API holds the poll until the result is ready:

Query parameterTypeRequiredDescription
waitintegerOptionalSeconds the API may hold the request while the job runs, from 0 to 20. Absent or 0: the API answers at once, as a normal poll. Any other value returns 422 with VALIDATION_ERROR.
  • Job already finished: the answer is immediate.
  • Job still running: the API holds the request until the result is ready or wait seconds have passed, whichever comes first, then answers exactly what a normal poll returns at that moment.
  • Early answers: the API can also answer before wait seconds with the job still running, for example when many long polls are open at once or during maintenance. Always check status. While it is created or in_progress, poll again with wait once Retry-After seconds have passed since the previous poll started.
  • Cost and limits: a long poll costs 0 credits and does not count toward your per-minute rate limit, like any poll.
  • Timeout: set your HTTP client timeout above wait, for example 30 seconds for wait=20. A shorter timeout cuts the request before the API answers.

The Trigger and poll loop uses wait=20 and this pacing rule: each result arrives as soon as it's ready, with far fewer requests than polling every few seconds.

Without wait: follow Retry-After

Without wait, each poll answers at once, so the pacing rule becomes a pause of Retry-After seconds between polls (2 when the header is missing), starting with the Retry-After of the response that created the job.

Poll at the Retry-After pace

// retryAfter: the Retry-After header of the response that created the job
async function pollUntilDone(webhookId, retryAfter = 2) {
  while (true) {
    await new Promise(r => setTimeout(r, retryAfter * 1000))
    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")
    retryAfter = Number(res.headers.get("Retry-After")) || 2 // 2 when the header is missing
  }
}

Inspect and retrieve results from the dashboard

To check a job without writing code, use Job Lookup in 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 result payload, and download it as JSON.

You can also open Job Lookup from the Webhook Events page: the action button on the right of each row opens the lookup pre-filled for that event.

Job Lookup lets you test polling end to end before you write any integration code, and it helps with support, ops and manual debugging. It calls the same API, so the dashboard shows exactly what your code receives 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

StatusCodeWhen
404WEBHOOK_EVENT_NOT_FOUNDThe webhookId doesn't exist or doesn't belong to your workspace
410WEBHOOK_EVENT_EXPIREDThe result has expired and is no longer retrievable
422VALIDATION_ERRORwait is not an integer from 0 to 20

Combining polling with a webhookUrl

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

  • You want webhooks as the primary path but need a fallback 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.

FAQ

How long does a live enrichment take?

All Reverse Contact live endpoints run in the background: they reply immediately with a webhookId and usually deliver the result within seconds. The aim is under 15 seconds per result, but processing can stretch to tens of minutes depending on source availability, queue load and retries, so live endpoints are not built for synchronous, real-time requests a user is waiting on.

Does polling for a result cost credits or count toward the rate limit?

No. Status checks on GET /v2/webhooks/:webhookId, with or without the wait parameter, cost 0 credits whatever the status returned, and polling does not count toward your per-minute rate limit (RPM). The credits billed for the job appear in creditsConsumed, which is 0 while the job is in progress and shows the final amount once it has completed.

How often should I poll for an async result?

Call GET /v2/webhooks/:webhookId?wait=20 in a loop until status is succeeded or errored, with an HTTP client timeout above the wait value, for example 30 seconds: each call answers as soon as the job ends, so the result arrives without delay and with few requests. Start two polls at least Retry-After seconds apart, or 2 seconds when the header is missing: a held poll already lasts longer, so the next one starts right away, and an answer that comes back early is followed by a pause for the rest of Retry-After. Without wait, the same rule means a pause of Retry-After seconds between polls; polling every 20 seconds instead delays each result by up to 20 seconds. See Polling frequency.

How long are polling results kept?

Results from GET /v2/webhooks/:webhookId 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 410 with WEBHOOK_EVENT_EXPIRED. Workspaces with longer retention requirements can reach out to support to extend this window.

Do I need a webhook receiver to get async enrichment results?

No. With polling, you can omit webhookUrl from the request body, save the webhookId the async endpoint returns, and call GET /v2/webhooks/:webhookId until status is succeeded or errored, with no public URL to expose, no receiver to deploy and no tunnel to maintain. If you do pass a webhookUrl, the result is delivered to that URL and remains retrievable via polling.