Behavior change:GET /listings no longer returns each listing's photo URLs (meta.photos) unless you ask for them with the new include_photos=true query parameter. num_photos is still returned.
Added is_hidden to the listing object returned by GET /listings, GET /listings/{listing_id}, GET /segments/{segment_id}/listings and the /sets/{set_id}/associated_listings endpoints. It is true for a listing that has been hidden in the Wheelhouse app. Hidden listings are still returned and ranked with exclude_inactive=true, so filter on is_hidden to leave them out.
Behavior change: a user with restricted editor access to a shared listing can no longer change its tags with PUT /listings/{listing_id}/tags, nor update it through the batch PUT /preferences. The Wheelhouse app refuses restricted editors tag changes and updates to several listings at once. The batch endpoint is refused however many listings a request names: the listing is reported in errors with status 403, and the rest of the batch still applies. Update a single listing's preferences with PUT /preferences/{listing_id}, which restricted editors can still use.
Behavior change:PUT /sets/{set_id}/associated_listings now requires manage access to each shared listing, as in the Wheelhouse app. A user with viewer access to a shared listing could previously associate it with a set, and now gets 403. Removing a listing from a set with DELETE is unchanged.
Owners, editors and managers are unaffected by either change.
Each Wheelhouse user can now hold up to 5 active RM API keys. Creating another returns an error until the user holds fewer than 5. Users already holding more keep all of them. Team members each have their own limit. The key behind MCP access is not counted. See Direct API integrations.
Added owner_user_id to GET /listings and GET /listings/kpis. It narrows either response to the listings owned by one Wheelhouse account, matching owner_user.id on the listing, so a team member can read or rank one team's portfolio at a time by passing a membership's user_id from GET /teams/memberships. It grants access to no listing you could not already reach, so an account whose listings you cannot access returns an empty list.
Documentation fix: listing_preferences on the listing object (GET /listings, GET /listings/{listing_id}) was documented as an unstructured object. It has always carried a summary of the listing's pricing preferences — automatic rate posting, base price and adjustment, fees, long-term discount percentages, guest counts, nickname, and the global minimum and maximum price and minimum stay where configured — and the schema now lists those fields. The schema also now marks as required the fields that are always present on the listing object (id, channel), on price calendar, last posted price and minimum stay calendar rows, and on the pricing tier, check-in/check-out, min/max price and neighborhood responses, and lists the two custom_type values a price recommendation can carry. No behavior or response-shape change.
Documentation fix: name on GET /listings/{listing_id}/pricing_tier listed a tier the endpoint never returns and omitted two it does. The values are Free, Pro Flex, Pro Flat, Pro Trial and Pro Onboarding. No behavior change.
Behavior change:amenities on GET /sets/candidates and GET /sets/{set_id}/listings is now an array of amenity names. It was previously a single comma-separated string.
Documentation fix: optional fields on those listing responses (including guests_included) have always been null when unknown. Custom rate responses have always returned currency as null for adjustment rates, and a weekday value as null when that day is unset. The schema now matches. No other response-shape change.
Documentation fix: several response fields have always been able to be null or to carry a shape the schema omitted. The schema now matches what the endpoints already return. No behavior or response-shape change. In particular: optional reservation fields (including comments and monetary amounts); tag description; reservation_id on the price calendar and calendar-day history (a string, the channel reservation ID); custom-rate periods, which do not include id or listing_id on read; custom calendar rules, which may send day_of_week_values instead of value and may include an id; calendar-rule priority, which can be fractional; and legacy days_after / days_one_sided_gap on adjacency rules.
Preferences now document and validate fee_adjustments: the estimated share of fees guests pay on top of the nightly rate (total_fee_percentage, -100 or more) and whether it is refreshed weekly from the listing's recent reservations (auto_update). Every preferences read returns it, null when nothing is configured, and PUT /preferences and PUT /preferences/{listing_id} take it. The object replaces the stored one as a whole, and null clears it. Behavior change: the object was previously stored as sent, without validation; a fee_adjustments that is not an object, or a total_fee_percentage below -100, is now rejected with 400. GET /listings/{listing_id}/fee_impact_calendar continues to report the setting as a multiplier.
Documentation fix: custom_type on GET /listings/{listing_id}/price_recommendations and the preferences preview response has always been null when the night is a Wheelhouse recommendation, not an empty string. The schema now marks the field nullable and the description matches. No behavior or response-shape change.
The bedrooms parameter on GET /market_report/{market_id}/time_series and /distribution no longer takes a fixed set of five groups. Which bedroom groups a market is broken down by is derived from the listings in it, so a market with many larger listings is broken down further (0, 1, 2, 3, 4, 5, 6+) than one without them (0, 1, 2+). Read a market's groups from the new GET /market_report/{market_id}/bedroom_groups, which also reports how many listings sit behind each one. A group the market has no data for is rejected with 400, and the error names the groups it does have. Existing calls keep working where the market is broken down past the group they name: bedrooms=4+ is answered from the 4 bedroom group in a market broken down into 4, 5 and 6+.
Breaking: the geometry fields have been removed from the GET /market_report response — both the market's own geometry (the polygons describing its boundary) and the per-entry geometry under postal_codes (each postal code's boundary). Anything drawing market or postal code outlines from this endpoint will find both absent; the rest of the response is unchanged, postal_codes[].latlong included, so a centroid is still available per postal code. Set boundaries are unaffected — boundary on the dynamic set endpoints still returns MultiPolygon coordinates.
Documented installing the Wheelhouse Plugin from https://github.com/pricemethod/wheelhouse-plugin for Claude Code, Codex, and Cursor.
GET /sets now takes name and kind filters, so an account with many sets can find one without paging through the whole list. name is a case-insensitive pattern in which * stands for any run of characters — *Palm Springs* matches the phrase anywhere in the name, and a value without a * has to match the name in full. kind takes one value or several comma-separated, and kind=other also matches sets that carry no kind, which is what those sets already report. Sending both filters returns the sets matching each of them, and either combines with the existing pagination parameters.
Documentation fix: GET /preferences/{listing_id}/changelog was titled as the changelog for a listing's preferences, which undersold it — it has always returned every change recorded against the listing, including prices posted, failed posts and calendar syncs, custom rates added, split or removed, a change of pricing engine, and reservations imported. The title now says so, the description lists what is covered, and GET /listings/{listing_id}/custom_rates now points here for custom rate history rather than leaving it to be inferred that it carries none. No behavior or response-shape change.
Behavior change: preference settings the listing's channel cannot act on are no longer stored. Previously a setting the channel had no support for was accepted, saved and logged to the changelog — so the write looked successful and the changelog confirmed it, but the value was never posted to the channel. Such settings are now discarded instead, matching what the Wheelhouse app has always done. The rest of the request still applies and the response is still a success.
Preference write responses now carry warnings, naming each setting that was not applied, why (not_supported_by_channel, or partially_supported_by_channel when only part of it was too narrow for the channel), and the attributes it covered. The array is present and empty when everything applied, so it can be read unconditionally. Applies to PUT /preferences, PUT /preferences/{listing_id}, PUT /preferences/{listing_id}/copy and POST /preferences/{listing_id}/preview.
Listings and preferences now report supported_settings — whether checkin_checkout, minimum stays and long-term discounts can be set, and which form of long-term discount the channel takes. Check it before sending a setting rather than after. It reflects per-account early access, so two listings on the same channel can differ. See the Listing Preferences section.
Every response now carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 additionally carries Retry-After. Your current standing arrives with work you were already doing, so pacing an integration no longer costs a request — and the 429 itself now tells you when to retry. See Rate Limits.
Added GET /usage, so an integration can inspect its own API usage rather than reading the audit log in the Wheelhouse app. It reports request volumes, error rates (including 429s) and response times across every RM API key on the account, broken down by day, by endpoint and by key, over a window of up to 7 days. Endpoint breakdowns are grouped by route pattern rather than exact path, so all calls to GET /listings/{listing_id}/notes count as one entry. Results are cached for a few minutes — see generated_at — and a window too busy to read in full is reported as window.truncated with the shorter window it actually covers.
Extended GET /listings/{listing_id}/kpis/monthly, /kpis/quarterly, and /kpis/yearly with additional metrics already persisted on the periodic stats tables: adr_fees, asking_rate, nights_available, nights_blocked, nights_bookable, nights_booked, revenue_available, revenue_blocked, revenue_fees, and revenue_fees_taxes. Names match the rolling KPI lexicon.
Added GET /sets/{set_id}/price_calendar, the API equivalent of the set calendar in the app. booking_probability_percent is bounded to 10–90 for unavailable nights, mirroring the app, which renders those extremes as <10% and >90% rather than asserting certainty about a listing it only scrapes; an available night reports 0. The rest: a night-by-night calendar for each active member listing of a set, with prices converted to one currency so comps priced differently are comparable. Paid sets only, and only for sets with at most 25 member listings; a larger set responds with 422. Both limits track where the data comes from: Wheelhouse requests the detailed per-night scraping that backs this endpoint only for paid sets at or under that size, so beyond it there would be little to return. The night object follows GET /listings/{listing_id}/price_calendar except that it carries no is_booked flag and no reservation_id, neither of which applies to a listing that is not yours. In their place, booking_probability_percent reports Wheelhouse's assessment that a night is booked rather than merely held, as a whole percentage.
Added PUT and DELETE /sets/{set_id}/listings, which add and remove a set's member listings — the comparison listings the set is built from. Previously members could only be chosen when the set was created with POST /sets, with no way to adjust them afterwards, so a set could not be refined once built. Identify listings by the internal listing_id values from GET /sets/candidates; only eligible comparison candidates can be added, the same restriction POST /sets applies. Both return the set's members grouped by status, matching GET /sets/{set_id}/listings, and both work on free and paid sets. Not to be confused with the associated_listings endpoints, which manage which of your own listings the set is compared against.
Fix:GET /sets/{set_id}/listings and GET, PUT, DELETE /sets/{set_id}/associated_listings returned 404 Not Found ("Dynamic set not found") for a free set. Building a set was therefore impossible before paying for it: you could not review which listings were in it, nor associate it with one of your own listings. All four now work on free and paid sets alike. The per-listing metrics returned by /listings were already available ungated through GET /sets/candidates, which returns the same object. The paid gate stays on the set-level analytics — aggregated_metrics, time_series, distribution and changelog.
Added canceled_at to the reservation object returned by GET /listings/{listing_id}/reservations. It carries the timestamp a reservation was cancelled, or null if it stands, and is set whenever status is Canceled — previously the status told you a reservation had been cancelled but not when. Note the single-l spelling, which matches the Canceled status value rather than the cancelled field of the PUT request body.
Fix:GET /segments/{segment_id}/listings and GET /segments/{segment_id}/aggregated_metrics evaluated a segment's filter only against listings the user owns, so a user whose portfolio is entirely made up of listings shared with them to manage got an empty result from every segment. Both endpoints now match owned and managed listings, consistent with GET /listings, and both accept include_managed_listings to control it (default true for RM API keys, false for channel integration keys).
Listings can now be identified by their Wheelhouse listing ID as well as by the channel's own listing ID. Pass channel=wheelhouse and listing_id is read as a Wheelhouse ID (the wheelhouse_id field of GET /listings, also returned as listing_id by GET /listings/kpis) rather than as a channel listing ID. Wheelhouse IDs are unique across channels, so no channel needs naming alongside them. This works everywhere a listing is identified, including the batched listing_ids parameters and copy_preferences_from. Under channel=wheelhouse a listing you cannot access returns 404, not 403, so the sequential ID space cannot be walked to discover other accounts' listings; the channel-listing-ID form keeps its existing 403. See Identifying Listings. Existing calls are unaffected — omitting channel=wheelhouse keeps the channel listing ID behavior.
Added the Webhooks endpoints: GET /webhook_events, GET /webhooks, POST /webhooks, GET /webhooks/{id}, PUT /webhooks/{id}, DELETE /webhooks/{id}, POST /webhooks/{id}/rotate_secret, and GET /webhooks/{id}/deliveries. Register an HTTPS endpoint to be notified when price recommendations change (recommendations.updated), when new reservations are ingested (reservations.ingested), or when new listing flags are detected (flags.detected), instead of polling for changes. Deliveries are batched over a five-minute window, name the affected listing_ids rather than carrying the changed values, and are signed with a per-subscription HMAC-SHA256 Wh-Signature header. See the Webhooks section for verification and retry semantics.
Added GET /listings/kpis, which returns one rolling-window KPI for every listing the authenticated user can access, ranked by value and paginated. It answers portfolio-wide questions — best and worst performers on a metric — in one call rather than one per listing. Choose the metric and window with metric and window; pass currency to convert monetary metrics before ranking so that portfolios spanning currencies order correctly. Each row carries listing_id (Wheelhouse's id), partner_listing_id (the channel's), value, currency, and updated_at. Comp-set metrics are not covered yet.
Documentation fix: GET /listings/{listing_id}/kpis/monthly, /kpis/quarterly, and /kpis/yearly were described as returning only history. They have always returned future periods as well — monthly spans up to 15 months back and up to 12 months forward, quarterly and yearly likewise extend up to 12 months ahead. The word "historical" has been dropped from their titles and each endpoint now documents its full window, with a note that current and future periods reflect bookings on the books plus asking rates for open nights and will therefore change as bookings come in. No behavior or response-shape change.
Behavior change:include_managed_listings on GET /listings now defaults to true. GET /listings returns every listing the authenticated user can access — owned listings plus listings shared with them to manage — where it previously returned owned listings only unless the parameter was passed explicitly. Pass include_managed_listings=false to keep the previous behavior. Use each listing's access_level and owner_user to tell the two apart.
Added owner_user to the listing response. It identifies the Wheelhouse account a listing belongs to (id, email, first_name, last_name), which makes it possible to tell whose portfolio each listing sits in when a response mixes owned and managed listings — for example GET /listings?include_managed_listings=true. Note that neither owner_name (the property owner name supplied by the channel) nor source_user_id (the host ID in the channel's own namespace) identifies a Wheelhouse account; their descriptions have been clarified accordingly.
Extended GET /listings/{listing_id}/kpis to cover the full Wheelhouse metrics lexicon, now served from a dedicated rolling-stats store. New metrics: adr_fees, asking_rate_fees, nights_bookable, nights_booked, nights_calendar, nights_percent_open, pickup_bookings, revenue_fees, revenue_fees_taxes, last_booked_days, lead_time, length_of_stay. nights_available, nights_blocked, revenue_available, revenue_blocked, and min_price_occurrence now also return backward-looking (N_0) periods; asking_rate_lowest and asking_rate_highest now return all periods in both directions; pickup now covers all backward-looking periods (7/14/21/30/60/90/180/365). Breaking:revenue_score is now keyed by cumulative forward-looking periods (0_7 … 0_365) instead of the non-overlapping 0_30/31_60/61_90 windows; the scalar last_booked_at field has been removed in favor of the last_booked_days metric; and each metric field is null until rolling stats have been generated for the listing.
Renamed the performance metrics returned by GET /sets/candidates and GET /sets/{set_id}/listings to the public Wheelhouse metrics lexicon, matching the other RM API KPI endpoints: anr → adr, apr → asking_rate, nrevenue → revenue, nrevpar → revpar, nrevpar_open → revpar_adjusted, open_occupancy → occupancy_adjusted, openness → nights_percent_open, los → length_of_stay, and *_nights → nights_* (each keeps its _365_0/_90_0 period suffix).
Added dynamic-set creation endpoints: GET /sets/candidates (search market listings with metrics near a location to build a set from), POST /sets (create a free set from selected listings), and POST /sets/{set_id}/upgrade (purchase the paid Dynamic Sets plan for a set to unlock its KPI endpoints). GET /sets and GET /sets/{set_id} now also return free sets, each carrying an is_paid flag.
Added GET /segments/{segment_id}/aggregated_metrics endpoint. Returns monthly performance metrics aggregated across the listings matched by a segment's filter, mirroring GET /sets/{set_id}/aggregated_metrics. Monetary values default to the currency of the segment's most common market.
Added GET /notification_settings and PUT /notification_settings endpoints. Users can now read which alerts they receive and enable or disable individual alert event/channel (in-app or email) settings via the API.
Added the Team endpoints: GET /teams/members, POST /teams/members/invite, DELETE /teams/members/{sub_user_id}, POST /teams/members/{sub_user_id}/auto_managed_listing_level, POST /teams/members/{sub_user_id}/assigned_segments, POST /teams/members/{sub_user_id}/refresh_auto_managed_listings, GET /teams/memberships, POST /teams/memberships/{master_user_id}, and DELETE /teams/memberships/{master_user_id} to manage team members and memberships.
Added POST /segments endpoint. Creates a new portfolio segment owned by the authenticated user. Accepts filter_backend to define the listing filter criteria.
Added PUT /segments/{segment_id} endpoint. Updates the name, description, filter_backend, and/or default status of an existing segment.
Added POST /listings/{listing_id}/sync endpoint. Triggers a manual sync that pushes the latest Wheelhouse price recommendations to the connected channel and refreshes reservation data — equivalent to the Sync button in the UI. Available on paid plans only; subject to a per-day rate limit and a 60-second debounce.
Added GET /listings/{listing_id}/base_price_history endpoint. Returns a daily log of Wheelhouse base price recommendations and effective base prices for a listing over the last 30 days (or a custom date range).
Added GET /listings/{listing_id}/calendar_day_history endpoint. Returns the history of price postings and calendar state snapshots for a specific stay date. Requires the Historical Price Changes feature on the listing's plan.
Extended GET /listings/{listing_id}/kpis with additional metrics: revenue_available, revenue_blocked, min_price_occurrence, occupancy_neighborhood, occupancy_neighborhood_pp, occupancy_neighborhood_ratio, occupancy_neighborhood_adjusted, occupancy_neighborhood_adjusted_pp, occupancy_neighborhood_adjusted_ratio (all forward-only, periods 7/14/21/30/60/90/180/365); comp_set_occupancy, comp_set_occupancy_adjusted, comp_set_revenue (forward-only, periods 7/30/60); pickup (backward-only, periods 7/14/30); asking_rate_lowest and asking_rate_highest (backward-only, period 365); revenue_score (non-overlapping windows 0–30, 31–60, 61–90 days); and scalar comp_set_count.
Split GET /listings/{listing_id}/kpis into four dedicated endpoints: GET /listings/{listing_id}/kpis (rolling-window metrics — bidirectional periods 7/14/21/30/60/90/180/365), GET /listings/{listing_id}/kpis/monthly (up to 15 months back incl. default dynamic-set metrics), GET /listings/{listing_id}/kpis/quarterly (up to 5 recent quarters), and GET /listings/{listing_id}/kpis/yearly (up to 5 years). Each response includes currency. The old days parameter is no longer accepted.
Added GET /notifications and POST /notifications/dismiss endpoints. Users can now list their active in-app notifications and dismiss specific ones by ID via the API.
Added optional expires_at field to PUT /listings/{listing_id}/custom_rates and PUT /listings/{listing_id}/bulk_custom_rates. Custom rates can now be set with an expiry timestamp after which they are no longer applied.
Added GET /listings/{listing_id}/fee_impact_calendar endpoint. Returns a per-date fee multiplier derived from the listing's estimated fee setting. Multiply any nightly price by the multiplier to get the fee-inclusive price.
Added GET /listings/{listing_id}/min_stay_calendar endpoint. Returns the per-date minimum stay derived from the listing's minimum stay rules for a given date range.
Added the Notes endpoints: GET /listings/{listing_id}/notes, POST /listings/{listing_id}/notes, PUT /listings/{listing_id}/notes/{note_id}, and DELETE /listings/{listing_id}/notes/{note_id} to manage the notes of a listing.
Added the Dynamic Sets endpoints: GET /sets, GET /sets/{set_id}, GET /sets/{set_id}/aggregated_metrics, GET /sets/{set_id}/listings, GET /sets/{set_id}/associated_listings, PUT /sets/{set_id}/associated_listings, DELETE /sets/{set_id}/associated_listings, GET /sets/{set_id}/changelog, GET /sets/{set_id}/time_series, and GET /sets/{set_id}/distribution. RM API users can access their paid dynamic sets.
Added GET /listings/{listing_id}/neighborhood/pricing endpoint to fetch daily neighborhood price data (median, p25, p75, listing count) for the listing's local cluster.
Added GET /listings/{listing_id}/neighborhood/occupancy endpoint to fetch daily neighborhood occupancy and booking model data for the listing's local cluster.
Added GET /listings/{listing_id}/last_posted_prices endpoint. Returns the last price posted to the channel per stay date. For available nights this is the most recently posted price; for booked nights it is the last price posted at or before the time the booking was recorded.
Added GET /market_report, GET /market_report/{market_id}/time_series, and GET /market_report/{market_id}/distribution endpoints. RM API users can access market-level data for markets in which they have listings.
All PUT endpoints now return 409 Conflict when a concurrent request for the same resource is already in progress. Retry the request after the concurrent request completes.
Added GET /listings/{listing_id}/price_calendar endpoint to fetch the price calendar for a single listing.
Initial release of the Wheelhouse Revenue Management API.
Endpoints: GET /listings, GET /listings/{listing_id}, GET /listings/{listing_id}/pricing_tier, GET /listings/{listing_id}/recent_changes, GET /listings/{listing_id}/kpis.
Endpoints: GET /listings/{listing_id}/price_recommendations, GET /listings/{listing_id}/base_price_recommendation, GET /listings/{listing_id}/checkin_checkout, GET /listings/{listing_id}/min_max_prices, GET /listings/{listing_id}/monthly_seasonality.
Endpoints: GET /preferences, PUT /preferences, GET /preferences/{listing_id}, PUT /preferences/{listing_id}, PUT /preferences/{listing_id}/copy, PUT /preferences/{listing_id}/{setting}, GET /preferences/{listing_id}/long_term_discounts, GET /preferences/{listing_id}/changelog, POST /preferences/{listing_id}/preview.
Endpoints: PUT /listings/{listing_id}/custom_rates, DELETE /listings/{listing_id}/custom_rates, PUT /listings/{listing_id}/bulk_custom_rates, DELETE /listings/{listing_id}/bulk_custom_rates.
Endpoints: GET /listings/{listing_id}/reservations (with date_filter_type parameter).
Endpoints: GET /listings/{listing_id}/tags, PUT /listings/{listing_id}/tags, GET /listings/{listing_id}/flags.
Endpoints: GET /segments, GET /segments/{segment_id}/listings.