Skip to content
StayingAPI
Docs

Response & error envelope

Every REST response is one of exactly three envelope shapes — success (200), async-accepted (202) and error (4xx/5xx). Learn it once; it is identical on every endpoint.

Success — HTTP 200

Only data varies by endpoint (see the unified schema). The meta block around it is the same everywhere.

200 OK
{
  "data": [ /* Property[] | object — the endpoint-specific payload */ ],
  "meta": {
    "requestId": "req_8sf…",
    "platforms": ["airbnb", "booking"],
    "cached": false,
    "partial": false,
    "creditsCharged": 8,
    "currency": "USD",
    "pagination": { "limit": 20, "cursor": null, "nextCursor": "eyJ…", "hasMore": true },
    "platformResults": [
      { "platform": "airbnb", "status": "ok", "creditsCharged": 4, "cached": false, "count": 18 },
      { "platform": "booking", "status": "ok", "creditsCharged": 4, "cached": true, "count": 20 }
    ],
    "warnings": []
  }
}

The meta block

FieldMeaning
requestIdUnique req_-prefixed id for this request; quote it in support tickets. Also the X-Request-Id header.
platformsThe platform(s) actually queried, in the order resolved.
cachedtrue only if the entire response was served from cache. Per-platform flags live in platformResults[].
partialtrue if a fan-out had ≥1 platform fail while ≥1 succeeded (see below).
creditsChargedTotal credits across platforms. 0 on any failed/empty leg and on any error.
currencyThe currency of monetary values in data — always echoed, never inferred.
paginationCursor block { limit, cursor, nextCursor, hasMore } on synchronous list responses; null on single-object endpoints and on async-returned (job) results.
platformResults[]One row per platform queried in a fan-out: status, creditsCharged, cached, count, error.
warnings[]Non-fatal advisories (may be empty). Tolerate unknown codes.

Async accepted — HTTP 202

When a live scrape is projected to run longer than ~8 seconds, the request returns a job handle instead of blocking. No credits are charged on the 202 — the work is billed once, on completion, via /v1/jobs/{jobId}. Cache hits are always served synchronously.

202 Accepted
{
  "data": { "jobId": "job_3kf…", "status": "pending", "pollUrl": "/v1/jobs/job_3kf…", "estimatedSeconds": 25 },
  "meta": { "requestId": "req_…", "creditsCharged": 0, "platforms": ["vrbo"] }
}

Poll GET /v1/jobs/{jobId} until data.status is completed. The endpoint payload then arrives at data.result (not data), with the full meta — credits, platformResults, warnings — reconstructed at the top level. Because the merged result is assembled once, meta.pagination is null on a job result. estimatedSeconds is a hint only: jobs commonly run tens of seconds and can exceed 240 seconds, so honour the Retry-After the poll returns.

A job can also fail. A failed job is returned as HTTP 200 (not a 4xx/5xx) with data.status set to failed and the failure nested at data.error — a { type, code, message, retryable } object. This is the one place a failure is delivered inside data rather than as a top-level error, so reliability code must branch on data.status — never on the HTTP status or a top-level error. Like every failure it is billed 0; error.retryable tells you whether a re-submit can succeed.

Poll — failed (HTTP 200)
// A FAILED async job — still HTTP 200 (not 4xx/5xx). The failure is nested at
// data.error with data.status "failed"; this is NOT the top-level { error } envelope.
{
  "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_…", "creditsCharged": 0, "platforms": ["vrbo"] }
}

Most consumers never touch a 202

Error — 4xx / 5xx

A synchronous response never carries both a top-level data and a top-level error — a 4xx/5xx is always the error envelope below. (The one nuance: a failed async job is delivered as an HTTP 200 success envelope whose data.status is failed and whose failure is nested at data.error — see async jobs above.) Field semantics — and the complete catalog — are on the errors page. creditsCharged is always 0 on an error.

400 Bad Request
{
  "error": {
    "type": "invalid_request",
    "code": "missing_parameter",
    "message": "checkOut is required when checkIn is provided.",
    "param": "checkOut",
    "requestId": "req_…",
    "creditsCharged": 0,
    "retryable": false,
    "docUrl": "https://stayingapi.com/docs/errors/missing_parameter"
  }
}

Partial fan-out semantics

This is the most important non-obvious rule in the envelope. A fan-out queries N platforms concurrently and merges their normalized results. If every platform fails, it is a top-level 503. But if at least one succeeds and at least one fails, you get HTTP 200 with:

Partial success
// HTTP 200 — airbnb succeeded, vrbo was blocked
{
  "data": [ /* 18 Airbnb Property objects only */ ],
  "meta": {
    "platforms": ["airbnb", "vrbo"],
    "partial": true,
    "creditsCharged": 4,
    "platformResults": [
      { "platform": "airbnb", "status": "ok", "creditsCharged": 4, "cached": false, "count": 18 },
      { "platform": "vrbo", "status": "failed", "creditsCharged": 0, "cached": false, "count": 0,
        "error": { "type": "upstream_unavailable", "code": "actor_blocked", "retryable": true,
                   "message": "All Vrbo sources failed or were blocked for this request." } }
    ],
    "warnings": [
      { "code": "platform_failed", "platform": "vrbo", "message": "Vrbo results omitted; not charged." }
    ]
  }
}

A failed platform is always free