ScreenshotNeo

BlogEngineering

How to Retrieve Asynchronous API Job Results

Save the job ID, poll safely, handle terminal states, and retrieve results from asynchronous APIs with resilient code and webhook patterns.

By the ScreenshotNeo team1 October 20269 min read

To retrieve an asynchronous API result, save the identifier returned when you submit the request, call the provider’s status or retrieval endpoint while the job is pending, and read the result only after a successful terminal state. Handle failure and cancellation separately. If the API supports webhooks, use the callback as a completion signal and retrieve the resource using its identifier.

There is no universal endpoint path or status vocabulary. One API may return queued and in_progress; another may return an operation resource with done: false. Use the exact response schema documented by your provider.

1. The asynchronous result pattern

  1. Submit the work.
  2. Store the returned response ID, job ID, or operation name immediately.
  3. Retrieve the job at the provider’s documented interval.
  4. Continue while the state is pending or running.
  5. When the state is terminal, check whether it succeeded, failed, or was cancelled.
  6. On success, read the inline result or follow the documented result or download URI.
job = submit_request()
job_id = job.id

repeat:
    job = retrieve_job(job_id)
    if job.status is pending or running:
        wait(provider_recommended_interval)
        continue
    if job.status is successful:
        return read_result(job)
    if job.status is failed or cancelled:
        handle_terminal_error(job)
        stop

Keep the identifier exact. Resource-style operation names can contain slashes or other characters, and a batch request may also require a per-item correlation key such as a documented custom_id.

2. Polling a job safely

Choose the provider’s status endpoint

Read the API reference before writing the loop. Confirm:

  • the submission response field containing the identifier;
  • the URL and HTTP method used to retrieve status;
  • pending, running, success, failure, and cancellation states;
  • where output or a download URI appears;
  • recommended polling intervals, rate limits, retention, and cancellation rules.

Use bounded backoff

Polling too quickly increases request load and can trigger rate limits. Poll no faster than the provider recommends. If no interval is documented, start with a modest delay, increase it with bounded exponential backoff, and stop after a maximum elapsed time that fits the job’s retention period.

delay = 2
for attempt in range(12):
    job = retrieve_job(job_id)
    if job.status in TERMINAL_STATES:
        return handle_terminal(job)
    sleep(delay)
    delay = min(delay * 2, 30)
raise TimeoutError("job did not finish before the client deadline")

A server-side wait operation can reduce request frequency and the delay between completion and notification. Google Compute Engine documents this pattern, but its wait call is bounded and may return before completion, so always inspect the returned state and continue when necessary.

3. Complete cURL example

The following example uses placeholder paths and fields because asynchronous APIs do not share one contract. Replace them with the values from your provider’s reference.

# Submit
submit=$(curl -sS -X POST https://api.example.com/v1/jobs \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"input":"example"}')

job_id=$(printf '%s' "$submit" | jq -r '.id')

# Poll until the provider reports a terminal state
while :; do
  job=$(curl -sS https://api.example.com/v1/jobs/"$job_id" \
    -H 'Authorization: Bearer YOUR_TOKEN')
  status=$(printf '%s' "$job" | jq -r '.status')

  case "$status" in
    queued|in_progress|pending|running)
      sleep 5
      ;;
    completed|succeeded|done)
      printf '%s\n' "$job" | jq '.result // .response // .downloadUri'
      break
      ;;
    failed|cancelled|canceled)
      printf '%s\n' "$job" | jq '.error // .errors'
      exit 1
      ;;
    *)
      echo "Unknown status: $status" >&2
      exit 2
      ;;
  esac
done

Do not copy the placeholder state names into production blindly. For example, Google long-running operations commonly expose done, while OpenAI background responses use states such as queued, in_progress, and completed.

4. Python: submit, poll, and retrieve

import time
import requests

BASE = "https://api.example.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_TOKEN"}

submit = requests.post(
    f"{BASE}/jobs",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"input": "example"},
    timeout=30,
)
submit.raise_for_status()
job = submit.json()
job_id = job["id"]

started = time.monotonic()
delay = 2
while True:
    if time.monotonic() - started > 900:
        raise TimeoutError("job exceeded the client deadline")

    response = requests.get(
        f"{BASE}/jobs/{job_id}",
        headers=HEADERS,
        timeout=30,
    )
    if response.status_code == 429:
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else delay)
        delay = min(delay * 2, 30)
        continue
    response.raise_for_status()
    job = response.json()
    status = job.get("status")

    if status in {"queued", "in_progress", "pending", "running"}:
        time.sleep(delay)
        delay = min(delay * 2, 30)
        continue

    if status in {"completed", "succeeded", "done"}:
        result = job.get("result") or job.get("response")
        if result is None and job.get("download_uri"):
            result_response = requests.get(job["download_uri"], timeout=90)
            result_response.raise_for_status()
            result = result_response.content
        print(result)
        break

    if status in {"failed", "cancelled", "canceled"}:
        raise RuntimeError(job.get("error", "asynchronous job failed"))

    raise RuntimeError(f"unrecognized provider status: {status}")

5. Node.js: submit, poll, and retrieve

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const headers = {
  Authorization: 'Bearer YOUR_TOKEN',
  'Content-Type': 'application/json'
};

const submitted = await fetch('https://api.example.com/v1/jobs', {
  method: 'POST',
  headers,
  body: JSON.stringify({ input: 'example' })
});
if (!submitted.ok) throw new Error(`submit failed: ${submitted.status}`);

const first = await submitted.json();
const jobId = first.id;
const deadline = Date.now() + 15 * 60 * 1000;
let delay = 2000;

while (Date.now() < deadline) {
  const response = await fetch(
    `https://api.example.com/v1/jobs/${encodeURIComponent(jobId)}`,
    { headers: { Authorization: headers.Authorization } }
  );

  if (response.status === 429) {
    const retryAfter = Number(response.headers.get('retry-after'));
    await sleep(Number.isFinite(retryAfter) ? retryAfter * 1000 : delay);
    delay = Math.min(delay * 2, 30000);
    continue;
  }
  if (!response.ok) throw new Error(`status request failed: ${response.status}`);

  const job = await response.json();
  if (['queued', 'in_progress', 'pending', 'running'].includes(job.status)) {
    await sleep(delay);
    delay = Math.min(delay * 2, 30000);
    continue;
  }
  if (['completed', 'succeeded', 'done'].includes(job.status)) {
    console.log(job.result ?? job.response ?? job.download_uri);
    break;
  }
  if (['failed', 'cancelled', 'canceled'].includes(job.status)) {
    throw new Error(JSON.stringify(job.error ?? job.errors ?? job));
  }
  throw new Error(`unknown status: ${job.status}`);
}
if (Date.now() >= deadline) throw new Error('job deadline exceeded');

6. Provider-specific examples

OpenAI Responses background mode

When background mode is enabled, retain the response ID. Retrieve the response while its status is queued or in_progress, then read output only when the status is completed. A failed or cancelled response must be handled as an error. OpenAI’s background guide describes temporary disk storage for polling and the effect of the request’s store setting; verify current retention requirements in the official background mode documentation.

Google Cloud long-running operations

Google Cloud APIs commonly return an operation name. Call the documented get endpoint with that name and inspect done. Continue while done is false. Once true, inspect the error field before consuming the response. Some Google APIs return a download URI after completion.

Google Compute Engine wait

A Compute Engine operation wait call can reduce request frequency and completion-notification latency compared with repeated get calls. It is best effort and bounded, so a response can still indicate that the operation is unfinished. Check state and call wait or get again as required.

Batch APIs

For asynchronous batches, preserve both the batch identifier and each request’s documented correlation key. OpenAI Batch uses a unique custom_id so completed results can be matched to their original inputs.

7. Webhooks instead of polling

A webhook lets the provider notify your server when work reaches a terminal state. It is useful for long jobs and server-side workflows, but it requires a reachable HTTPS endpoint, signature verification where supported, replay-safe processing, and a recovery path for missed events.

POST /webhooks/jobs

1. Read the raw request body.
2. Verify the provider signature and timestamp.
3. Reject invalid or stale deliveries.
4. Record the event ID idempotently.
5. Extract the job or response identifier.
6. Retrieve the job from the provider.
7. Process success, failure, or cancellation.
8. Return a 2xx response quickly.

The event may contain only a reference, not the complete result. OpenAI’s webhook pattern uses the event’s response ID to retrieve the response. Gemini documents webhooks for supported asynchronous workloads. Treat the webhook as a notification, then use the provider’s retrieval endpoint as the source of truth. Keep polling available so a missed callback does not leave work permanently unknown.

Pattern Best fit Tradeoffs
Polling Simple clients, short jobs, or APIs without webhooks Repeated requests, possible rate limits, and completion delay based on interval
Webhook Server applications with a secure receiver and long-running jobs Endpoint operations, signature checks, retries, replay protection, and missed-event recovery

8. Reliability checklist

  • Persist the identifier before beginning status checks.
  • Store a per-item correlation key for batches.
  • Use provider-recommended intervals and honor Retry-After.
  • Set a client deadline and stop polling after retention can no longer guarantee retrieval.
  • Retry transient network failures and 5xx responses with bounded backoff.
  • Do not retry a terminal failure or cancellation as if it were pending.
  • Validate the result schema before handing data to downstream code.
  • Make webhook handling idempotent and verify signatures.
  • Persist enough state to recover after a process restart.

9. Troubleshooting

Symptom Likely cause Fix
404 or unknown job Wrong endpoint, malformed operation name, expired resource, or wrong project Use the exact identifier returned by submission, URL-encode path parameters, and check retention and project scope.
Status never changes Polling the wrong resource or treating a wait response as final Log the complete status response and confirm the provider’s state field and endpoint.
Rate limit errors Polling too frequently or many workers checking the same job Back off, honor Retry-After, deduplicate pollers, or use a webhook.
Result field is empty Operation is terminal but failed, or output is behind a URI Inspect error details and follow the documented result or download URI.
Webhook accepted but no result processed Event contains only an ID, or signature parsing used a transformed body Verify the raw body, authenticate the event, then retrieve the resource by ID.
Duplicate webhook work Provider retry or receiver timeout Record event IDs or job-state transitions and make processing idempotent.
Client times out HTTP timeout is shorter than job duration Separate submission and retrieval requests; use a durable worker, bounded polling deadline, or webhook.

10. Performance, reliability, and cost

Polling interval controls a tradeoff between request volume and how quickly the client notices completion. A provider wait method or webhook can reduce unnecessary status calls. For many jobs, centralize polling so each job has one active poller, persist state in a queue or database, and cap concurrent retrieval requests.

Do not infer universal completion times, uptime, or cost from another provider’s examples. OpenAI describes temporary storage for background polling, and one Google example uses a 10-second interval; those are provider-specific details, not general guarantees. API pricing may charge submission, execution, retrieval, or data transfer differently, so consult the target service’s pricing page.

11. Or skip the browser setup

If the asynchronous result you need is a website screenshot, ScreenshotNeo returns the capture directly from one GET request. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed.

For asynchronous workflows, ScreenshotNeo also provides async jobs with signed webhooks, bulk capture for up to 100 URLs per call, caching with a chosen TTL, and a usage API. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.

See the ScreenshotNeo API documentation for all options. A direct capture looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There is a free plan with 1,000 screenshots per month and no card required. Paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

12. FAQ

How do I get the result of an asynchronous API request?

Save the returned identifier, retrieve the job until it reaches a terminal state, verify success, and then read the provider’s result field or follow its result URI.

How do I check the status of a job ID?

Call the provider’s documented status endpoint with the exact job ID or operation name. Do not assume that a generic /status path or common state names exist.

Can a webhook tell me when my API job is done?

Yes, when the provider supports completion webhooks. Verify the signature, process events idempotently, and retrieve the resource if the event contains only an identifier.

Should I poll forever?

No. Set a deadline based on the provider’s retention and retry rules. After the deadline, persist the identifier for a recovery worker or surface an operational timeout.

What does a completed operation with an error mean?

Terminal means processing stopped; it does not guarantee success. Inspect the provider’s error field before consuming output.