# Staying API — full documentation corpus Generated from MDX docs. Pages are concatenated in nav order. --- ## Staying API ## REST for Airbnb stay data. Staying API gives you programmatic, structured access to Airbnb listings — exposed through a small, predictable REST surface. One API key, one canonical `Stay` shape, every sub-resource available as its own URL. Sign up, create an API key, run a `curl`. The [Quickstart](/quickstart/) walks through the first call. Bearer tokens. Optional OAuth + MCP scope. See [Authentication](/authentication/). Every endpoint and field documented. Start with [Stays](/api/stay/). MCP server, OpenAPI 3.1, llms.txt, agent-skills bundle. See [AI agents](/ai-agents/). ## What you get back Every `Stay` row is normalized to the same shape, regardless of which upstream record fed it. ```json { "data": { "id": "1135700964697993602", "url": "https://airbnb.com/rooms/1135700964697993602", "title": "Beachfront flat in Estoril", "property_type": "Apartment", "room_type": "Entire home", "person_capacity": 4, "star_rating": 4.92, "reviews_count": 117, "location": { "latitude": 38.71, "longitude": -9.40, "address": "Cascais, Portugal" }, "host": { "name": "Maria", "is_superhost": true }, "pricing": { "price": 184, "currency": "USD" }, "photos": [{ "url": "https://...", "caption": null }], "amenities": [{ "category": "Bathroom", "items": ["Hair dryer", "Shampoo"] }] }, "request_id": "req_abc" } ``` ## Next steps - Walk through the [Quickstart](/quickstart/). - Read about [Authentication](/authentication/) and [Rate limits](/rate-limits/). - Browse the [Stays endpoint](/api/stay/) and the [Search endpoint](/api/search/). - Plug us into an AI agent — see the [AI agents guide](/ai-agents/). --- ## Quickstart This page walks you through your first request to Staying API. ## 1. Sign up [Create an account](https://stayingapi.com/signup). You get 100 free credits and no card is required. ## 2. Create an API key From the dashboard, go to **API keys** and click **Create key**. The full secret is shown exactly once — copy it into your password manager before clicking dismiss. API keys look like `sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` (32 base64url characters after the prefix). ## 3. Make your first call ```bash curl https://api.stayingapi.com/v1/stays/1135700964697993602 \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ```python --- ## Authentication Staying API uses **bearer tokens** in the `Authorization` header. Every API key starts with the prefix `sk_` followed by 32 base64url characters. ## REST ```http GET /v1/stays/1135700964697993602 HTTP/1.1 Host: api.stayingapi.com Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ```bash curl https://api.stayingapi.com/v1/stays/1135700964697993602 \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ## Managing keys Issue and revoke keys from the dashboard at [/app/keys](https://stayingapi.com/app/keys). - Plain-text secrets are shown **only once**, at creation. Store them in a vault. - Revoking a key takes effect within seconds. Active integrations stop working immediately after revoke; we return `401 invalid_key`. - Each key has a name (your label) and a prefix you can use for searching logs. ## MCP / OAuth For Model Context Protocol clients we also support OAuth 2.1 with PKCE: - Authorization endpoint: `https://api.stayingapi.com/oauth/authorize` - Token endpoint: `https://api.stayingapi.com/oauth/token` - Scope: `mcp:access` - Discovery: [/.well-known/oauth-authorization-server](https://api.stayingapi.com/.well-known/oauth-authorization-server) Once you complete the OAuth handshake the access token is a long-lived API key, indistinguishable from the ones issued in the dashboard. The MCP server at `https://api.stayingapi.com/mcp` accepts it in the same `Authorization: Bearer` header. ## Best practices - **Never** ship a secret to a client. Keep keys server-side. - Rotate periodically. Revoke and re-create rather than amending. - Use one key per environment (`dev`, `staging`, `production`). ## Next - [Errors](/errors/) - [Rate limits](/rate-limits/) - [AI agents guide](/ai-agents/) --- ## Errors Every error response is JSON with a stable shape: ```json { "error": { "status": 401, "code": "invalid_key", "message": "API key is invalid or revoked." }, "request_id": "req_abc" } ``` Forward the `request_id` to support when contacting us — it lets us trace exactly which call failed. ## Error catalogue | Status | Code | Meaning | |---|---|---| | 400 | `invalid_address` | Missing or too-short address parameter. | | 400 | `invalid_url` | URL parameter missing or fails our URL validator. | | 400 | `invalid_id` | Listing id is malformed. | | 400 | `invalid_filter` | A filter key is unknown or value is out of range. | | 400 | `missing_field` | Required field absent from request body. | | 401 | `unauthenticated` | No `Authorization` header. | | 401 | `invalid_key` | API key not found or revoked. | | 403 | `forbidden` | Authenticated, but lacks scope to access this resource. | | 404 | `not_found` | Listing or job not found. | | 409 | `conflict` | Resource already exists (e.g. duplicate webhook URL). | | 422 | `validation_failed` | Body fails JSON schema validation. | | 429 | `rate_limited` | RPM threshold exceeded for your plan. | | 429 | `quota_exceeded` | Account is out of credits. Top up or wait for renewal. | | 500 | `internal_error` | Unhandled server-side error. We get paged. | | 502 | `upstream_failure` | Upstream lookup failed. Retry-safe with exponential backoff. | | 504 | `upstream_timeout` | Upstream took too long. Retry-safe. | ## Retrying - `429 rate_limited` — back off until your next minute and retry. - `429 quota_exceeded` — do not retry. Top up or wait. - `5xx` — retry with exponential backoff, capped at 60s. ```python --- ## Rate limits We apply rate limits **per API key**, measured in requests per minute (RPM). | Plan | RPM | |---|---| | Free | 20 | | Monthly | 200 | | Annual | 300 | | Enterprise | 1500 (custom) | ## Headers we set Every response includes the current rate-limit posture: ```http x-ratelimit-limit: 200 x-ratelimit-remaining: 187 x-ratelimit-reset: 1716040860 ``` `x-ratelimit-reset` is a unix timestamp at which your bucket refills. ## When you exceed the limit We return HTTP 429 with the JSON error body: ```json { "error": { "status": 429, "code": "rate_limited", "message": "Try again in 32 seconds." }, "request_id": "req_abc" } ``` A `retry-after` header gives you the wait in seconds. Sleep, then retry — your client SHOULD honor it. ## Quotas Independent of RPM, every call charges credits from your plan balance. When the balance hits zero we return 429 `quota_exceeded` instead. Top up or wait for the next cycle to resume. ## Best practices - Don't poll faster than 1 request per second per endpoint. - For bulk work, use the [async-jobs path](/async-jobs/). Pagination cursors are cheaper than re-querying. - Cache idempotent reads on your side too. We cache detail rows for 24h internally; you don't need to re-fetch every hour. ## Next - [Pagination](/pagination/) - [Async jobs](/async-jobs/) - [Errors](/errors/) --- ## Pagination List endpoints — `/v1/search`, `/v1/listings/in/{city}`, and friends — return paged results. We return `meta.has_more` and a cursor; you pass the cursor on the next request. ## Response shape ```json { "data": [ /* Array */ ], "meta": { "total": 217, "count": 50, "limit": 50, "offset": 0, "has_more": true, "next_cursor": "eyJzZWVuIjoyMTd9" }, "request_id": "req_abc" } ``` `next_cursor` is opaque — treat it as a string token; we do not commit to its internal structure. ## Walking pages ```python --- ## Output formats Endpoints return JSON by default. The `/v1/datasets/{id}` endpoint streams data in either JSON or CSV — useful for downloading large async job results. ## JSON (default) ```bash curl https://api.stayingapi.com/v1/stays/1135700964697993602 \ -H "Authorization: Bearer sk_live_..." ``` Response: ```json { "data": { "id": "1135700964697993602", "title": "Beachfront flat in Estoril" } } ``` ## CSV The dataset endpoint accepts a `format` query parameter. CSV streaming is faster for downloads bigger than ~5MB. ```bash curl "https://api.stayingapi.com/v1/datasets/ds_abc?format=csv" \ -H "Authorization: Bearer sk_live_..." > stays.csv ``` ```javascript const res = await fetch("https://api.stayingapi.com/v1/datasets/ds_abc?format=csv", { headers: { Authorization: "Bearer sk_live_..." }, }); await pipeline(res.body, fs.createWriteStream("stays.csv")); ``` ## Content negotiation Both `Accept` headers and `format=` query params work: - `Accept: application/json` (default) - `Accept: text/csv` - `?format=json` - `?format=csv` If both are present, the query string wins. ## Next - [Field projection](/field-projection/) - [Async jobs](/async-jobs/) - [Pagination](/pagination/) --- ## Field projection Every detail and search response supports field projection via the `fields` query parameter. Pass a comma-separated list of dot-paths from the canonical `Stay` shape. ## Quick example ```bash # Just id, title, and the per-night price curl "https://api.stayingapi.com/v1/stays/1135700964697993602?fields=id,title,pricing.price" \ -H "Authorization: Bearer sk_live_..." ``` ```json { "data": { "id": "1135700964697993602", "title": "Beachfront flat in Estoril", "pricing": { "price": 184 } } } ``` ## How projection works - Each entry can be a top-level field (`id`) or a dot-path (`pricing.price`). - Wildcards are not supported — list every leaf you want. - Order of fields in the response is not guaranteed (JSON objects are unordered). ## Supported root fields The canonical `Stay` shape has these top-level keys: `id`, `url`, `title`, `description`, `property_type`, `room_type`, `person_capacity`, `star_rating`, `location`, `host`, `pricing`, `reviews_count`, `rating_breakdown`, `photos`, `reviews`, `amenities`, `availability`. See the [Stays endpoint](/api/stay/) for the full schema. ## Search projection The same parameter works on search results — it's applied per-row. ```bash curl -X POST https://api.stayingapi.com/v1/search?fields=id,title,location.city \ -H "Authorization: Bearer sk_live_..." \ -H "content-type: application/json" \ -d '{"location":"Austin, TX"}' ``` ## Next - [Output formats](/output-formats/) - [Stays endpoint](/api/stay/) - [Search endpoint](/api/search/) --- ## Async jobs For searches that return more than 240 rows, or bulk lookups across many ids, Staying API runs the work asynchronously. You queue a job, poll its status, and download the dataset when complete. ## Lifecycle ``` queued → running → complete (or → failed) ``` - `queued` — accepted; waiting for a runner. - `running` — in flight. - `complete` — ready to fetch via `/v1/datasets/{dataset_id}`. - `failed` — see `error.code` on the job for the reason. ## Create a job ```bash curl -X POST https://api.stayingapi.com/v1/jobs \ -H "Authorization: Bearer sk_live_..." \ -H "content-type: application/json" \ -d '{ "template_id": "search-by-location-and-filters", "params": { "location": "Lisbon, Portugal", "filters": { "minBeds": 2 } }, "rowLimit": 2000 }' ``` Response: ```json { "data": { "id": "job_abc123", "status": "queued", "credits_estimated": 2000, "created_at": "2026-05-16T22:30:00Z" }, "request_id": "req_..." } ``` ## Poll for status ```bash curl https://api.stayingapi.com/v1/jobs/job_abc123 \ -H "Authorization: Bearer sk_live_..." ``` Poll every 2–5 seconds. We rate-limit polling more loosely than direct lookups. ## Fetch the dataset ```bash curl "https://api.stayingapi.com/v1/datasets/ds_xyz?format=csv" \ -H "Authorization: Bearer sk_live_..." > stays.csv ``` JSON also works (`format=json`). ## Webhooks instead of polling Register a webhook listening to `job.completed`; we'll POST to your URL the moment the dataset is ready. See the [Webhooks guide](/webhooks-guide/). ## Next - [Webhooks guide](/webhooks-guide/) - [Jobs endpoint reference](/api/jobs/) - [Pagination](/pagination/) --- ## Webhooks guide Webhooks let Staying API push events to your service the moment they happen. Every delivery is signed with an HMAC so you can verify it came from us. ## Events | Event | Payload | |---|---| | `stay.refreshed` | A cached stay was refreshed (id, new `_fetched_at`). | | `stay.cache_miss` | A real-time fetch was triggered (id, latency). | | `job.completed` | An async job finished. Payload has `job_id`, `dataset_id`, `rows`. | | `job.failed` | An async job failed. Payload has `job_id`, `error`. | | `credit.threshold` | You crossed 50% or 100% of granted credits this cycle. | | `key.created` / `key.revoked` | Audit events for key lifecycle. | ## Register From the dashboard: **Webhooks → New webhook**. Pick which events to subscribe to; we'll show your signing secret exactly once. Or via the API: ```bash curl -X POST https://api.stayingapi.com/v1/webhooks \ -H "Authorization: Bearer sk_live_..." \ -H "content-type: application/json" \ -d '{"url":"https://example.com/hooks/staying","events":["stay.refreshed","job.completed"]}' ``` ## Delivery shape ```http POST /your-hook HTTP/1.1 host: example.com x-stayingapi-signature: t=1716040800,v1=8b8c2bdac9... content-type: application/json { "event": "stay.refreshed", "id": "evt_abc", "created_at": "2026-05-16T22:30:00Z", "data": { "id": "1135700964697993602" } } ``` We retry twice on non-2xx (1s and 6s delay) and time out after 10 seconds per attempt. Persistent failures are recorded for forensics; you can replay from the dashboard. ## Verify signatures See [recipes/verify-webhook](/recipes/verify-webhook/) for a full Node example. ## Next - [Webhooks endpoint reference](/api/webhooks/) - [Verify webhook recipe](/recipes/verify-webhook/) - [Async jobs](/async-jobs/) --- ## For AI agents Staying API is designed to be used by AI agents as a first-class consumer. ## MCP server The Model Context Protocol server lives at `https://api.stayingapi.com/mcp`. It speaks the 2025-06-18 MCP spec and exposes 5 tools: | Tool | Purpose | Cost | |---|---|---| | `lookup_stay_by_id` | Get a stay by Airbnb id. | 1 credit | | `lookup_stay_by_url` | Get a stay by airbnb.com/rooms/... URL. | 1 credit | | `search_stays` | Search by location, dates + filters (maxItems ≤ 250). | 1 credit per result | | `get_stay_photos` | Photos array for a listing. | 1 credit | | `get_stay_reviews` | Reviews + rating breakdown for a listing. | 1 credit | Authenticate with `Authorization: Bearer sk_...` or connect via OAuth 2.1 — on claude.ai (or Claude Desktop) go to Settings → Connectors → Add custom connector and paste the server URL; the consent flow issues a key scoped `mcp:access` automatically. Setup instructions for Claude Desktop, Claude Code, Cursor, ChatGPT, and the OpenAI Agents SDK are in the [dashboard](/app/mcp). ## Discovery We publish RFC 8288 link relations on every page, so an agent that GETs the homepage finds: - `` → [/openapi.json](https://stayingapi.com/openapi.json) - `` → [/quickstart/](/quickstart/) - `` → `https://api.stayingapi.com/mcp` - `` → [/.well-known/mcp/server-card.json](https://stayingapi.com/.well-known/mcp/server-card.json) - `` → [/.well-known/agent-skills/index.json](https://stayingapi.com/.well-known/agent-skills/index.json) - `` → metadata for OAuth handshake. ## llms.txt and llms-full.txt - [llms.txt](https://stayingapi.com/llms.txt) — short, structured summary. - [llms-full.txt](https://stayingapi.com/llms-full.txt) — every docs page concatenated for LLM context windows. ## OpenAPI 3.1 [openapi.json](https://stayingapi.com/openapi.json) covers every route with full schema components, security schemes, and examples. Compatible with every codegen tool (Stainless, openapi-typescript, Swagger UI). ## WebMCP If you load `https://stayingapi.com/` in a Chromium-based agent, the page exposes `navigator.modelContext` tools for `navigate_to`, `open_signup`, `open_pricing`, and `open_mcp_setup`. See [webmcp.js](https://stayingapi.com/webmcp.js). ## Next - [Quickstart](/quickstart/) - [Webhooks guide](/webhooks-guide/) - [Authentication](/authentication/) --- ## Stays The Stay resource is the heart of the API. Every other route returns the same canonical shape. ## Endpoints ### `GET /v1/stays/{id}` Get a stay by listing id. ```bash curl https://api.stayingapi.com/v1/stays/1135700964697993602 \ -H "Authorization: Bearer sk_live_..." ``` ### `GET /v1/stays/by-url` Get a stay by airbnb.com/rooms URL. ```bash curl "https://api.stayingapi.com/v1/stays/by-url?url=https://airbnb.com/rooms/1135700964697993602" \ -H "Authorization: Bearer sk_live_..." ``` ### Sub-resources Slice the cached row without re-fetching. All return the same `{ data, request_id }` envelope, with `data` narrowed. | Path | Returns | |---|---| | `/v1/stays/{id}/photos` | `{ id, photos[] }` | | `/v1/stays/{id}/reviews` | `{ id, reviews_count, rating_breakdown, reviews[] }` | | `/v1/stays/{id}/host` | `{ id, host }` | | `/v1/stays/{id}/amenities` | `{ id, amenities[] }` | | `/v1/stays/{id}/availability` | `{ id, availability[] }` | | `/v1/stays/{id}/pricing` | `{ id, pricing }` | | `/v1/stays/{id}/location` | `{ id, location }` | | `/v1/stays/{id}/rating` | `{ id, star_rating, reviews_count, rating_breakdown }` | ## Canonical Stay schema ```typescript interface Stay { id: string; url: string; title: string; description: string | null; property_type: string | null; room_type: string | null; person_capacity: number | null; star_rating: number | null; location: { latitude: number | null; longitude: number | null; address: string | null; city: string | null; country: string | null; }; host: { id: string | null; name: string | null; is_superhost: boolean; profile_image_url: string | null; profile_url: string | null; rating: number | null; }; pricing: { price: number | null; currency: string | null; rate_type: string | null; breakdown: { label: string; amount: number }[] | null; }; reviews_count: number; rating_breakdown: { accuracy: number | null; cleanliness: number | null; communication: number | null; location: number | null; value: number | null; check_in: number | null; guest_satisfaction: number | null; }; photos: { url: string; caption: string | null }[]; reviews: { id: string; author_name: string; rating: number; text: string; created_at: string }[]; amenities: { category: string; items: string[] }[]; availability: { date: string; available: boolean; min_nights: number | null; price: number | null }[]; } ``` ## Example response ```json { "data": { "id": "1135700964697993602", "title": "Beachfront flat in Estoril", "property_type": "Apartment", "room_type": "Entire home", "person_capacity": 4, "star_rating": 4.92, "reviews_count": 117, "location": { "latitude": 38.71, "longitude": -9.40, "address": "Cascais, Portugal" }, "host": { "name": "Maria", "is_superhost": true }, "pricing": { "price": 184, "currency": "USD", "rate_type": "per_night" } } } ``` ## Next - [Search endpoint](/api/search/) - [Listings presets](/api/listings/) - [Field projection](/field-projection/) --- ## Listings The listings preset routes are shortcuts that wrap `/v1/search` with sensible defaults. Good for SEO-friendly URLs, link previews, and quick prototypes. ## Endpoints ### `GET /v1/listings/in/{city}` All currently-bookable listings in a city slug. ```bash curl https://api.stayingapi.com/v1/listings/in/lisbon-portugal \ -H "Authorization: Bearer sk_live_..." ``` The slug is URL-safe lowercase with hyphens (`lisbon-portugal`, `austin-tx`). ### `GET /v1/listings/superhost/{city}` Listings filtered to superhost-only. ```bash curl https://api.stayingapi.com/v1/listings/superhost/austin-tx \ -H "Authorization: Bearer sk_live_..." ``` ## Response shape Same as `/v1/search` — an array of canonical `Stay` rows with `meta` pagination. ```json { "data": [ { "id": "1135700964697993602", "title": "Beachfront flat in Estoril" } ], "meta": { "total": 217, "count": 50, "has_more": true } } ``` ## Filters You cannot override the preset's filters via query string — for free-form filter control use `/v1/search` directly. The presets exist for stable, cacheable URLs. ## Next - [Search endpoint](/api/search/) - [Pagination](/pagination/) --- ## Search `POST /v1/search` is the discovery endpoint. Use it to find stays matching any combination of location, dates, prices, capacities, and host attributes. ## Request body ```typescript type SearchRequest = { location?: string; // free-text location query, e.g. "Lisbon, Portugal" startUrls?: string[]; // OR pass airbnb.com/s/... search URLs (not /rooms URLs) bbox?: { north: number; south: number; east: number; west: number }; checkIn?: string; // ISO date checkOut?: string; // ISO date currency?: string; // ISO code, default "USD" filters?: { priceMin?: number; priceMax?: number; minBeds?: number; minBedrooms?: number; minBathrooms?: number; adults?: number; children?: number; infants?: number; pets?: boolean; isSuperhost?: boolean; }; include?: ("host" | "amenities")[]; // opt-in enrichments maxItems?: number; // ≤240 sync, larger goes async limit?: number; // page size (default 50, max 100) cursor?: string; // pagination cursor from prior page }; ``` ## Example — text query ```bash curl -X POST https://api.stayingapi.com/v1/search \ -H "Authorization: Bearer sk_live_..." \ -H "content-type: application/json" \ -d '{ "location": "Lisbon, Portugal", "checkIn": "2026-07-01", "checkOut": "2026-07-06", "filters": { "minBeds": 2, "priceMax": 250 } }' ``` ## Example — bounding box ```bash curl -X POST https://api.stayingapi.com/v1/search \ -H "Authorization: Bearer sk_live_..." \ -d '{ "bbox": { "north": 38.78, "south": 38.68, "east": -9.10, "west": -9.21 } }' ``` ## Response shape ```json { "data": [ /* Array */ ], "meta": { "total": 217, "count": 50, "limit": 50, "offset": 0, "has_more": true, "next_cursor": "eyJ..." }, "request_id": "req_..." } ``` ## Notes on completeness Search results carry every field that the upstream's discovery surface knows about — title, photos, host, rating, pricing. Some fields (full reviews, availability calendar, full description) require a follow-up `/v1/stays/{id}` lookup; see the [search-then-detail recipe](/recipes/search-then-detail/). ## Hard caps A single search is capped at 240 results. If you request `maxItems > 240`, we split the query and run it as an [async job](/async-jobs/). ## Next - [Async jobs](/async-jobs/) - [Search then detail recipe](/recipes/search-then-detail/) - [Pagination](/pagination/) --- ## Jobs ## `POST /v1/jobs` Queue an async job. ```bash curl -X POST https://api.stayingapi.com/v1/jobs \ -H "Authorization: Bearer sk_live_..." \ -H "content-type: application/json" \ -d '{ "template_id": "search-by-location-and-filters", "params": { "location": "Lisbon, Portugal", "filters": { "minBeds": 2 } }, "rowLimit": 2000 }' ``` Response: ```json { "data": { "id": "job_abc123", "status": "queued", "credits_estimated": 2000, "created_at": "2026-05-16T22:30:00Z" } } ``` ## `GET /v1/jobs/{job_id}` Poll status. ```bash curl https://api.stayingapi.com/v1/jobs/job_abc123 \ -H "Authorization: Bearer sk_live_..." ``` ```json { "data": { "id": "job_abc123", "status": "complete", "dataset_id": "ds_xyz", "rows": 1837, "credits_charged": 1837, "completed_at": "2026-05-16T22:33:14Z" } } ``` ## `GET /v1/datasets/{dataset_id}` Download the completed dataset. `format=json` (default) or `format=csv`. ```bash curl "https://api.stayingapi.com/v1/datasets/ds_xyz?format=csv" \ -H "Authorization: Bearer sk_live_..." > stays.csv ``` ## Notes - Polling is rate-limited more loosely than direct lookups; 2–5 seconds is fine. - Datasets are kept for 7 days after job completion. - Failed jobs include `error.code` and `error.message`. ## Next - [Async jobs guide](/async-jobs/) - [Webhooks](/api/webhooks/) - [Pagination](/pagination/) --- ## Webhooks ## `POST /v1/webhooks` Register a new webhook. ```bash curl -X POST https://api.stayingapi.com/v1/webhooks \ -H "Authorization: Bearer sk_live_..." \ -H "content-type: application/json" \ -d '{ "url": "https://example.com/hooks/staying", "events": ["stay.refreshed", "job.completed"] }' ``` Response (the `signing_secret` is shown only here): ```json { "data": { "id": "whk_abc", "url": "https://example.com/hooks/staying", "events": ["stay.refreshed", "job.completed"], "signing_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "enabled": true } } ``` ## `GET /v1/webhooks` List your webhooks. Signing secrets are masked. ```bash curl https://api.stayingapi.com/v1/webhooks \ -H "Authorization: Bearer sk_live_..." ``` ## `PUT /v1/webhooks/{id}` Update (enable / disable, change events, rotate secret). ```bash curl -X PUT https://api.stayingapi.com/v1/webhooks/whk_abc \ -H "Authorization: Bearer sk_live_..." \ -d '{"enabled":false}' ``` ## `DELETE /v1/webhooks/{id}` Delete. ```bash curl -X DELETE https://api.stayingapi.com/v1/webhooks/whk_abc \ -H "Authorization: Bearer sk_live_..." ``` ## Signature header ```http x-stayingapi-signature: t=1716040800,v1=8b8c2bdac9... ``` Where `t` is the unix timestamp and `v1` is `HMAC-SHA256(t.rawBody, signing_secret)`. Reject deliveries older than 5 minutes to prevent replay attacks. See [recipes/verify-webhook](/recipes/verify-webhook/) for code. ## Retries We retry on non-2xx responses up to 2 times (initial + 2), with 1s and 6s backoff. After 3 failures the delivery is recorded for replay from the dashboard. ## Next - [Webhooks guide](/webhooks-guide/) - [Verify webhook recipe](/recipes/verify-webhook/) - [Errors](/errors/) --- ## Account ## `GET /v1/me` Return your account profile, plan, and balance. ```bash curl https://api.stayingapi.com/v1/me \ -H "Authorization: Bearer sk_live_..." ``` ```json { "data": { "account_id": "acc_abc", "email": "you@example.com", "plan": "monthly", "status": "active", "balance": 380, "current_period_end": "2026-06-16T00:00:00Z" } } ``` ## `GET /v1/usage` Recent usage records (last 200 by default). ```bash curl "https://api.stayingapi.com/v1/usage?limit=50" \ -H "Authorization: Bearer sk_live_..." ``` Returns an array of `{ endpoint, status_code, units, created_at, request_id }`. ## `GET /v1/keys` List your active API keys. Secrets are never returned after creation. ```bash curl https://api.stayingapi.com/v1/keys \ -H "Authorization: Bearer sk_live_..." ``` ## Next - [Authentication](/authentication/) - [Errors](/errors/) - [Rate limits](/rate-limits/) --- ## Full stay lookup This recipe shows how to grab a complete `Stay` row in a single request. ## The call ```bash curl https://api.stayingapi.com/v1/stays/1135700964697993602 \ -H "Authorization: Bearer sk_live_..." ``` ```python --- ## Search then detail Search results are intentionally thinner than detail rows — they don't include full reviews, availability calendars, or full descriptions. This recipe shows the standard two-step flow for going from "find candidates" to "compare deeply". ## Step 1 — Search ```python - **Cost**: a SEARCH page is ~12 credits for 50 results; a DETAIL is 1 credit each. If you do detail-on-all, you'd pay 50 extra credits for stays you'll probably discard anyway. - **Latency**: search is one upstream call. Detail-on-all is N parallel calls. - **Repeat-lookup speed**: subsequent detail calls for the same listing return in tens of milliseconds. ## Batching detail calls If you have ≤25 ids you want to compare at once, the [no-code dashboard](/quickstart/)'s "multi-listing-comparison" template wraps the loop for you with a CSV download. Programmatically, just parallelise with `asyncio.gather` or `Promise.all`. ```javascript const ids = ["1135...", "2245...", "3355..."]; const details = await Promise.all( ids.map((id) => fetch(`https://api.stayingapi.com/v1/stays/${id}`, { headers: { Authorization: "Bearer sk_live_..." }, }).then((r) => r.json()) ) ); ``` ## Next - [Full stay lookup](/recipes/full-stay/) - [Async jobs](/async-jobs/) — for searches that need more than 240 rows - [Pagination](/pagination/) --- ## Verify webhook signatures Every outbound webhook delivery includes an `x-stayingapi-signature` header. To trust the payload came from us, you must: 1. Parse the header to extract `t` (timestamp) and `v1` (signature). 2. Recompute `HMAC-SHA256(t.rawBody, signing_secret)`. 3. Timing-safe compare it to `v1`. 4. Reject if the timestamp is older than 5 minutes. The `signing_secret` is the value shown to you exactly once when you created the webhook in the dashboard or via `POST /v1/webhooks`. ## Node (express) ```javascript const app = express(); const SECRET = process.env.STAYINGAPI_WEBHOOK_SECRET; app.post( "/hooks/staying", express.raw({ type: "application/json" }), (req, res) => { const sig = req.header("x-stayingapi-signature"); if (!sig) return res.status(401).send("missing signature"); const [tPart, v1Part] = sig.split(","); const t = tPart.split("=")[1]; const v1 = v1Part.split("=")[1]; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) { return res.status(401).send("timestamp too old"); } const signed = `${t}.${req.body.toString("utf8")}`; const expected = crypto.createHmac("sha256", SECRET).update(signed).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) { return res.status(401).send("bad signature"); } const event = JSON.parse(req.body.toString("utf8")); console.log("event:", event.event); res.status(200).send("ok"); } ); app.listen(3000); ``` ## Python (flask) ```python if ts, _ := strconv.ParseInt(t, 10, 64); time.Now().Unix()-ts > 300 { http.Error(w, "stale", 401) return } body, _ := io.ReadAll(r.Body) mac := hmac.New(sha256.New, secret) mac.Write([]byte(t + "." + string(body))) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(v1), []byte(expected)) { http.Error(w, "bad sig", 401) return } w.Write([]byte("ok")) } func main() { http.HandleFunc("/hooks/staying", hook) http.ListenAndServe(":3000", nil) } ``` ## Next - [Webhooks guide](/webhooks-guide/) - [Webhooks endpoint](/api/webhooks/) --- ## Trademark and affiliation ## We are not Airbnb. StayingAPI is not affiliated with, endorsed by, or sponsored by Airbnb, Inc. Airbnb is a registered trademark of Airbnb, Inc. StayingAPI accesses publicly available data through automated tools and exposes it via a stable REST API. ## What this means - We do **not** speak for Airbnb. Inaccuracies in our data are our problem to fix, not theirs. - We do **not** sell access to Airbnb's internal systems. We do not have credentials. We access publicly visible information programmatically, the same way a browser would. - Our brand mark, "Staying API", is our own; it deliberately does not use any Airbnb logo, colours, or design language. If you re-use our materials, do not imply otherwise. ## Use of the Airbnb name We refer to Airbnb in copy, documentation, and metadata strictly to identify the public data source we expose. This is permitted under nominative fair use: > Nominative fair use allows reference to another's trademark to identify the > trademark owner's product or service, provided (a) the product or service is > not readily identifiable without use of the trademark, (b) only so much of the > trademark is used as is reasonably necessary, and (c) the use does not suggest > sponsorship or endorsement by the trademark holder. We follow all three guardrails. ## Contact If you represent Airbnb, Inc. and have concerns about our usage, please reach us at support@stayingapi.com. ## Next - [Authentication](/authentication/) - [Errors](/errors/)