Endpoint reference
Async jobs
Poll a long-running scrape returned as a 202. Polling is always free; the underlying work is billed once, on successful completion.
/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
// 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.
// 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.
// 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.
estimatedSecondsprojects the run; a job usually finishes in tens of seconds but can exceed 240 seconds on a slow platform. Honour theRetry-Afterheader 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 returns404 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"withcreditsCharged: 0— bad data is free, even asynchronously — and the reason atdata.error(withretryable), not a top-levelerror. See On failure.
The SDK handles this for you
maxWaitMs is 600000 (10 min), which covers jobs that run several minutes; raise it on the call options for exceptionally slow live scrapes, or lower it to fail fast. Use waitForJob(jobId) or { awaitJob: false } only when you want to manage polling yourself.