Skip to content
StayingAPI
Docs

Async jobs

Poll a long-running scrape returned as a 202. Polling is always free; the underlying work is billed once, on successful completion.

GET/v1/jobs/{jobId}

When a request is projected to run longer than ~8 seconds it returns a 202 with a jobId. Poll this endpoint until data.status is completed or failed. Status is one of pending | running | completed | failed.

While the job runs

Poll — running
// While running — HTTP 200, polling is free
{
  "data": { "jobId": "job_3kf…", "status": "running", "pollUrl": "/v1/jobs/job_3kf…", "estimatedSeconds": 12 },
  "meta": { "requestId": "req_jb…", "creditsCharged": 0, "platforms": ["vrbo"] }
}

On completion

The completed payload arrives in data.result in the same unified schema the synchronous endpoint would return, and meta.creditsCharged reports the credits for the work — charged exactly once, here.

Poll — completed
// On success — HTTP 200; credits for the work are charged HERE, once
{
  "data": { "jobId": "job_3kf…", "status": "completed", "result": [ /* the endpoint's payload */ ] },
  "meta": {
    "requestId": "req_jb2…", "platforms": ["vrbo"], "creditsCharged": 4, "currency": "USD",
    "platformResults": [ { "platform": "vrbo", "status": "ok", "creditsCharged": 4, "cached": false, "count": 15 } ],
    "warnings": []
  }
}

On failure

A failed job is still HTTP 200 — the failure is nested at data.error (a { type, code, message, retryable } object), not the top-level error envelope. So detect failure by checking data.status === "failed", never by the HTTP status. meta.creditsCharged is 0 (failed work is free) and data.error.retryable tells you whether re-submitting the request can succeed.

Poll — failed (HTTP 200)
// On failure — STILL HTTP 200 (not 4xx/5xx). The reason is nested at data.error,
// NOT the top-level { error } envelope. Branch on data.status === "failed".
{
  "data": {
    "jobId": "job_3kf…",
    "status": "failed",
    "error": {
      "type": "upstream_unavailable",
      "code": "all_actors_failed",
      "message": "All sources for this request failed or were blocked. No credits charged.",
      "retryable": true
    }
  },
  "meta": { "requestId": "req_jb3…", "creditsCharged": 0, "platforms": ["vrbo"] }
}

Code samples

curl -sS "https://api.stayingapi.com/v1/jobs/job_3kf…" \
  -H "Authorization: Bearer $STAYINGAPI_KEY"

Rules to know

  • Timing is a hint, not a guarantee. estimatedSeconds projects the run; a job usually finishes in tens of seconds but can exceed 240 seconds on a slow platform. Honour the Retry-After header on each poll instead of a tight loop, and budget your total wait for minutes, not seconds.
  • List results lose their cursor. A completed job carries meta.pagination: null — cursor pagination only applies to synchronously-returned (sandbox / cache-hit) list responses.
  • Polling is always 0 credits. The work is billed once, on success — never on the 202 and never on a poll.
  • Results are retained 24 hours (jobs.expires_at); after that the id returns 404 job_not_found.
  • Ownership is enforced. A jobId that belongs to another user returns the same 404 as a non-existent one — the id is not a capability.
  • A failed job returns status: "failed" with creditsCharged: 0 — bad data is free, even asynchronously — and the reason at data.error (with retryable), not a top-level error. See On failure.

The SDK handles this for you