repull 0.2.25 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/lib/repull/api/airbnb_api.rb +38 -23
  3. data/lib/repull/api/booking_com_api.rb +9 -2
  4. data/lib/repull/api/connect_api.rb +6 -6
  5. data/lib/repull/api/conversations_api.rb +3 -0
  6. data/lib/repull/api/listings_api.rb +2 -2
  7. data/lib/repull/api/properties_api.rb +5 -2
  8. data/lib/repull/api/reservations_api.rb +96 -6
  9. data/lib/repull/api/reviews_api.rb +3 -0
  10. data/lib/repull/api/vrbo_api.rb +11 -4
  11. data/lib/repull/models/airbnb_listing.rb +13 -2
  12. data/lib/repull/models/airbnb_listing_details_write_request.rb +109 -5
  13. data/lib/repull/models/airbnb_listing_details_write_request_check_in_option.rb +1 -1
  14. data/lib/repull/models/airbnb_permits_write_request_permits_inner.rb +2 -2
  15. data/lib/repull/models/airbnb_pricing_write_request.rb +30 -2
  16. data/lib/repull/models/airbnb_pricing_write_request_fees_inner.rb +291 -0
  17. data/lib/repull/models/booking_property_listings_inner.rb +57 -1
  18. data/lib/repull/models/cancel_reservation200_response.rb +1 -1
  19. data/lib/repull/models/connect_session_completed_event.rb +281 -0
  20. data/lib/repull/models/connect_session_completed_payload.rb +210 -0
  21. data/lib/repull/models/connect_status.rb +35 -4
  22. data/lib/repull/models/connect_status_capabilities.rb +148 -0
  23. data/lib/repull/models/connection.rb +28 -5
  24. data/lib/repull/models/connection_action.rb +188 -0
  25. data/lib/repull/models/conversation.rb +12 -1
  26. data/lib/repull/models/conversation_detail.rb +12 -1
  27. data/lib/repull/models/create_connect_session_request.rb +40 -2
  28. data/lib/repull/models/create_connection_request.rb +46 -8
  29. data/lib/repull/models/error_error.rb +13 -1
  30. data/lib/repull/models/{cancel_reservation200_response_pms.rb → error_error_listings_inner.rb} +36 -30
  31. data/lib/repull/models/get_booking_extranet_login_status200_response.rb +2 -1
  32. data/lib/repull/models/list_listing_units200_response.rb +2 -1
  33. data/lib/repull/models/listing.rb +23 -2
  34. data/lib/repull/models/listing_capabilities.rb +148 -0
  35. data/lib/repull/models/property.rb +26 -3
  36. data/lib/repull/models/record_account.rb +170 -0
  37. data/lib/repull/models/reservation.rb +12 -1
  38. data/lib/repull/models/reservation_capabilities.rb +230 -0
  39. data/lib/repull/models/reservation_create_request.rb +172 -33
  40. data/lib/repull/models/reservation_create_response.rb +6 -6
  41. data/lib/repull/models/reservation_create_response_unit.rb +1 -1
  42. data/lib/repull/models/{reservation_create_response_pms.rb → reservation_pms_outcome.rb} +38 -8
  43. data/lib/repull/models/reservation_pms_outcome_quote.rb +168 -0
  44. data/lib/repull/models/{cancel_reservation200_response_pms_errors_inner.rb → reservation_pms_section_error.rb} +16 -16
  45. data/lib/repull/models/reservation_quote_request.rb +312 -0
  46. data/lib/repull/models/reservation_quote_response.rb +228 -0
  47. data/lib/repull/models/reservation_quote_response_breakdown.rb +175 -0
  48. data/lib/repull/models/reservation_update_response.rb +16 -6
  49. data/lib/repull/models/review.rb +12 -1
  50. data/lib/repull/models/submit_smoobu_credentials_request.rb +2 -5
  51. data/lib/repull/models/submit_smoobu_credentials_request_credentials.rb +193 -0
  52. data/lib/repull/models/update_airbnb_checkin_guide200_response.rb +147 -0
  53. data/lib/repull/models/update_airbnb_checkin_guide200_response_data.rb +177 -0
  54. data/lib/repull/models/{airbnb_permits_write_request_permits_inner_answers_value.rb → update_airbnb_checkin_guide200_response_data_steps_inner.rb} +24 -43
  55. data/lib/repull/models/update_airbnb_checkin_guide_request.rb +186 -0
  56. data/lib/repull/models/update_airbnb_checkin_guide_request_steps_inner.rb +182 -0
  57. data/lib/repull/models/webhook_event.rb +2 -0
  58. data/lib/repull/models/webhook_event_type.rb +2 -1
  59. data/lib/repull/version.rb +1 -1
  60. data/lib/repull.rb +21 -4
  61. data/openapi/v1.json +2221 -340
  62. data/scripts/regen.sh +1 -1
  63. metadata +23 -6
data/openapi/v1.json CHANGED
@@ -151,17 +151,30 @@
151
151
  "schemas": {
152
152
  "Property": {
153
153
  "type": "object",
154
- "description": "A vacation rental property in your Repull workspace. Backed by the core `listings` row \u2014 enriched per-PMS fields (bedrooms, property type, provider id, etc.) live in provider-specific detail tables and are NOT returned here.\n\nField availability differs by endpoint:\n- `channels` is returned by the list endpoint (`GET /v1/properties`) only.\n- `latitude`, `longitude`, `createdAt`, and `amenities` are returned by the detail endpoint (`GET /v1/properties/{id}`) only. `amenities` requires `?include=amenities`.\n\nAn **inactive** property (`status: inactive`) appears only in the list endpoint, and only when `?status=inactive|all` asks for it. Such a row carries identity fields only \u2014 `id`, `name`, `status`, `lifecycleStatus`, `channels`, `updatedAt` \u2014 so every other field is absent until the property is activated. Every other endpoint answers `403 listing_inactive` for it.",
154
+ "description": "A vacation rental property in your Repull workspace. Backed by the core `listings` row \u2014 enriched per-PMS fields (bedrooms, property type, provider id, etc.) live in provider-specific detail tables and are NOT returned here.\n\nField availability differs by endpoint:\n- `channels` is returned by the list endpoint (`GET /v1/properties`) only.\n- `latitude`, `longitude`, `createdAt`, and `amenities` are returned by the detail endpoint (`GET /v1/properties/{id}`) only. `amenities` requires `?include=amenities`.\n\nAn **inactive** property (`status: inactive`) appears only in the list endpoint, and only when `?status=inactive|all` asks for it. Such a row carries identity fields only \u2014 `id`, `name`, `city`, `status`, `inactiveReason`, `lifecycleStatus`, `channels`, `accounts`, `updatedAt` \u2014 so every other field is absent until the property is activated. `inactiveReason` is `plan_limit` (held back by the plan; activating needs a free slot or an upgrade), `unlisted_on_airbnb`, or `deactivated` (switched off by you). Every other endpoint answers `403 listing_inactive` for it.",
155
155
  "properties": {
156
+ "accounts": {
157
+ "type": "array",
158
+ "items": {
159
+ "$ref": "#/components/schemas/RecordAccount"
160
+ },
161
+ "description": "The connected account the property belongs to on each channel it is on. List endpoint."
162
+ },
156
163
  "id": {
157
164
  "type": "string",
158
165
  "description": "Internal Repull property ID. Equal to the listing id (`listings.id`); the same integer is used as `listingId` on reservations and `propertyId` on availability."
159
166
  },
160
167
  "name": {
161
168
  "type": "string",
162
- "description": "Property name",
169
+ "description": "Property name \u2014 the host's internal nickname.",
163
170
  "example": "Oceanview Suite #3"
164
171
  },
172
+ "publicName": {
173
+ "type": "string",
174
+ "nullable": true,
175
+ "description": "The title guests see on the channel (e.g. the Airbnb listing title). `name` is the host's internal nickname for the listing; show `publicName` in anything a guest or end user reads. Present on inactive rows too.",
176
+ "example": "Centre Oxford bright single room D"
177
+ },
165
178
  "address": {
166
179
  "type": "string",
167
180
  "nullable": true,
@@ -537,6 +550,14 @@
537
550
  "type": "object",
538
551
  "description": "A booking/reservation from a connected PMS. Identical shape between list-row (`GET /v1/reservations`) and detail (`GET /v1/reservations/{id}`) \u2014 SDK consumers can use the same type for both.\n\nThe canonical (post-2026-05) shape uses nested `primaryGuest`, `occupancy`, `financials` blocks. The legacy flat fields (`guestId`, `totalPrice`, `currency`, `guestDetails`) remain populated for back-compat and are marked `deprecated` here. New consumers should read from the nested blocks; existing consumers continue to work unchanged.",
539
552
  "properties": {
553
+ "account": {
554
+ "allOf": [
555
+ {
556
+ "$ref": "#/components/schemas/RecordAccount"
557
+ }
558
+ ],
559
+ "description": "The connected account this reservation belongs to. List endpoint."
560
+ },
540
561
  "id": {
541
562
  "type": "string",
542
563
  "description": "Internal Repull reservation ID"
@@ -977,6 +998,14 @@
977
998
  "type": "object",
978
999
  "description": "Channel-agnostic message thread between the host workspace and a guest. Returned by `GET /v1/conversations`. The `id` is the internal Repull thread id (integer) \u2014 pass it back as the `{id}` path param on detail / messages calls.",
979
1000
  "properties": {
1001
+ "account": {
1002
+ "allOf": [
1003
+ {
1004
+ "$ref": "#/components/schemas/RecordAccount"
1005
+ }
1006
+ ],
1007
+ "description": "The connected account this conversation belongs to. List endpoint."
1008
+ },
980
1009
  "id": {
981
1010
  "type": "string"
982
1011
  },
@@ -1319,6 +1348,14 @@
1319
1348
  "type": "object",
1320
1349
  "description": "A guest or host review unified across channels. Returned by `GET /v1/reviews` and `GET /v1/reviews/{id}`. Includes every channel's reviews once they have been imported.",
1321
1350
  "properties": {
1351
+ "account": {
1352
+ "allOf": [
1353
+ {
1354
+ "$ref": "#/components/schemas/RecordAccount"
1355
+ }
1356
+ ],
1357
+ "description": "The connected account this review belongs to. List endpoint."
1358
+ },
1322
1359
  "id": {
1323
1360
  "type": "string",
1324
1361
  "description": "Internal Repull review id \u2014 pass back to `/v1/reviews/{id}`."
@@ -1439,7 +1476,8 @@
1439
1476
  },
1440
1477
  "status": {
1441
1478
  "type": "string",
1442
- "example": "active"
1479
+ "example": "active",
1480
+ "description": "`active` \u2014 connected and working. `pending` \u2014 still settling. `needs_permissions` \u2014 connected but the host must grant more access before it works (see `action`/`fixUrl`). An `active` connection can also carry an `action` (e.g. a Smoobu legacy API key that must be replaced with a key + secret before October 31, 2026). `error` \u2014 the last operation failed. `disconnected` \u2014 revoked or superseded."
1443
1481
  },
1444
1482
  "externalAccountId": {
1445
1483
  "type": "string",
@@ -1457,6 +1495,45 @@
1457
1495
  }
1458
1496
  ],
1459
1497
  "description": "Host metadata for the linked account. Currently populated for Airbnb only; null for other providers."
1498
+ },
1499
+ "action": {
1500
+ "nullable": true,
1501
+ "allOf": [
1502
+ {
1503
+ "$ref": "#/components/schemas/ConnectionAction"
1504
+ }
1505
+ ],
1506
+ "description": "Set when the host must do something before the connection works (e.g. grant the invited Booking.com Extranet user full access). `null` when no action is pending."
1507
+ },
1508
+ "fixUrl": {
1509
+ "type": "string",
1510
+ "format": "uri",
1511
+ "nullable": true,
1512
+ "description": "Durable link that reopens the hosted Connect flow bound to this account on the fix screen \u2014 send the host here to resolve `action`. Present only when `action.required` is true; `null` otherwise."
1513
+ }
1514
+ }
1515
+ },
1516
+ "ConnectionAction": {
1517
+ "type": "object",
1518
+ "description": "A host action a connection needs before it can work.",
1519
+ "required": [
1520
+ "required"
1521
+ ],
1522
+ "properties": {
1523
+ "required": {
1524
+ "type": "boolean",
1525
+ "description": "Whether a host action is pending."
1526
+ },
1527
+ "reason": {
1528
+ "type": "string",
1529
+ "nullable": true,
1530
+ "example": "needs_permissions",
1531
+ "description": "Machine-readable reason, stable for programmatic handling."
1532
+ },
1533
+ "message": {
1534
+ "type": "string",
1535
+ "nullable": true,
1536
+ "description": "Host-facing one-liner describing what to do."
1460
1537
  }
1461
1538
  }
1462
1539
  },
@@ -2092,9 +2169,33 @@
2092
2169
  ],
2093
2170
  "description": "PMS connections only: what the app may change in the PMS. Change it with `PATCH /v1/connect/{provider}/write-policy`."
2094
2171
  },
2172
+ "capabilities": {
2173
+ "type": "object",
2174
+ "description": "PMS providers only. `reservations`: which reservation writes the API performs on this connection's listings \u2014 the connector's support combined with `writePolicy`. When `connected` is false, what the connector supports once connected.",
2175
+ "properties": {
2176
+ "reservations": {
2177
+ "$ref": "#/components/schemas/ReservationCapabilities"
2178
+ }
2179
+ }
2180
+ },
2095
2181
  "dataFreshness": {
2096
2182
  "type": "object",
2097
2183
  "description": "Vrbo only: the same freshness envelope the Airbnb read endpoints return, per account and in aggregate. Its reason is never_synced until a mapping is confirmed and importing while upcoming bookings come in."
2184
+ },
2185
+ "action": {
2186
+ "nullable": true,
2187
+ "allOf": [
2188
+ {
2189
+ "$ref": "#/components/schemas/ConnectionAction"
2190
+ }
2191
+ ],
2192
+ "description": "Smoobu only: set to `{ required: true, reason: \"reauth_required\", message }` when the connection still uses a legacy single API key, which Smoobu stops accepting on October 31, 2026. `null` once it is on an API key + secret."
2193
+ },
2194
+ "fixUrl": {
2195
+ "type": "string",
2196
+ "format": "uri",
2197
+ "nullable": true,
2198
+ "description": "Smoobu only: durable link to the hosted Smoobu form where the host pastes a new API key + secret. Submitting it updates this same connection (`id` unchanged). Present only when `action.required` is true."
2098
2199
  }
2099
2200
  }
2100
2201
  },
@@ -2491,6 +2592,7 @@
2491
2592
  "listing.reactivated",
2492
2593
  "calendar.updated",
2493
2594
  "account.created",
2595
+ "connect.session.completed",
2494
2596
  "account.disconnected",
2495
2597
  "review.created",
2496
2598
  "review.responded",
@@ -3543,6 +3645,46 @@
3543
3645
  }
3544
3646
  }
3545
3647
  },
3648
+ "ConnectSessionCompletedPayload": {
3649
+ "type": "object",
3650
+ "description": "Payload for `connect.session.completed`. A user finished a Connect session, on any channel or PMS. Use `state` (your token from session creation) or `sessionId` to tie the connection to your own user; the account it names is keyed the same way as every other event's `account` block.",
3651
+ "properties": {
3652
+ "sessionId": {
3653
+ "type": "string",
3654
+ "example": "cs_abc123"
3655
+ },
3656
+ "state": {
3657
+ "type": "string",
3658
+ "nullable": true,
3659
+ "description": "The `state` you passed when creating the session.",
3660
+ "example": "user_8421"
3661
+ },
3662
+ "provider": {
3663
+ "type": "string",
3664
+ "nullable": true,
3665
+ "description": "Channel or PMS: airbnb, booking, booking_extranet, vrbo, plumguide, hostaway, \u2026",
3666
+ "example": "vrbo"
3667
+ },
3668
+ "externalAccountId": {
3669
+ "type": "string",
3670
+ "nullable": true,
3671
+ "description": "The provider's own account id \u2014 Airbnb host id, Booking.com hotel id, Vrbo account id, or the PMS account.",
3672
+ "example": "36"
3673
+ },
3674
+ "connectionId": {
3675
+ "type": "integer",
3676
+ "nullable": true,
3677
+ "description": "Repull connection id, when the account has one (the `X-Account-Id` value)."
3678
+ },
3679
+ "purpose": {
3680
+ "type": "string"
3681
+ },
3682
+ "completedAt": {
3683
+ "type": "string",
3684
+ "format": "date-time"
3685
+ }
3686
+ }
3687
+ },
3546
3688
  "AccountDisconnectedPayload": {
3547
3689
  "type": "object",
3548
3690
  "description": "Payload for `account.disconnected`. A PMS or channel connection was revoked, expired, or rejected by the upstream provider.",
@@ -3892,6 +4034,29 @@
3892
4034
  }
3893
4035
  }
3894
4036
  },
4037
+ "RecordAccount": {
4038
+ "type": "object",
4039
+ "nullable": true,
4040
+ "description": "The connected account a record belongs to \u2014 keyed exactly like the webhook `account` block and `connect.session.completed`, so one `provider:externalAccountId` key routes reads and events to the same user. `null` when it cannot be resolved (never guessed).",
4041
+ "properties": {
4042
+ "provider": {
4043
+ "type": "string",
4044
+ "example": "airbnb",
4045
+ "description": "airbnb, booking, booking_extranet, vrbo, or the PMS id (hostaway, cloudbeds, \u2026)."
4046
+ },
4047
+ "externalAccountId": {
4048
+ "type": "string",
4049
+ "example": "79730216",
4050
+ "description": "The provider's own account id \u2014 Airbnb host id, Booking.com hotel id, Extranet login, Vrbo account, or the PMS account."
4051
+ },
4052
+ "connectionId": {
4053
+ "type": "string",
4054
+ "nullable": true,
4055
+ "description": "Repull connection id (`X-Account-Id`), when the account has one. A string, like every id in API responses.",
4056
+ "example": "126"
4057
+ }
4058
+ }
4059
+ },
3895
4060
  "WebhookEventAccount": {
3896
4061
  "type": "object",
3897
4062
  "nullable": true,
@@ -4599,6 +4764,42 @@
4599
4764
  }
4600
4765
  }
4601
4766
  },
4767
+ "ConnectSessionCompletedEvent": {
4768
+ "type": "object",
4769
+ "required": [
4770
+ "event",
4771
+ "eventId",
4772
+ "apiVersion",
4773
+ "timestamp",
4774
+ "data"
4775
+ ],
4776
+ "properties": {
4777
+ "event": {
4778
+ "type": "string",
4779
+ "description": "The event name. This field is `event`, not `type`."
4780
+ },
4781
+ "eventId": {
4782
+ "type": "string",
4783
+ "format": "uuid",
4784
+ "description": "Stable across every delivery and replay of this logical event \u2014 dedupe on it."
4785
+ },
4786
+ "apiVersion": {
4787
+ "type": "string",
4788
+ "example": "2026-04"
4789
+ },
4790
+ "timestamp": {
4791
+ "type": "string",
4792
+ "format": "date-time",
4793
+ "description": "When this delivery was built."
4794
+ },
4795
+ "account": {
4796
+ "$ref": "#/components/schemas/WebhookEventAccount"
4797
+ },
4798
+ "data": {
4799
+ "$ref": "#/components/schemas/ConnectSessionCompletedPayload"
4800
+ }
4801
+ }
4802
+ },
4602
4803
  "AccountDisconnectedEvent": {
4603
4804
  "type": "object",
4604
4805
  "required": [
@@ -5156,6 +5357,9 @@
5156
5357
  {
5157
5358
  "$ref": "#/components/schemas/AccountCreatedEvent"
5158
5359
  },
5360
+ {
5361
+ "$ref": "#/components/schemas/ConnectSessionCompletedEvent"
5362
+ },
5159
5363
  {
5160
5364
  "$ref": "#/components/schemas/AccountDisconnectedEvent"
5161
5365
  },
@@ -5215,6 +5419,7 @@
5215
5419
  "listing.reactivated": "#/components/schemas/ListingReactivatedEvent",
5216
5420
  "calendar.updated": "#/components/schemas/CalendarUpdatedEvent",
5217
5421
  "account.created": "#/components/schemas/AccountCreatedEvent",
5422
+ "connect.session.completed": "#/components/schemas/ConnectSessionCompletedEvent",
5218
5423
  "account.disconnected": "#/components/schemas/AccountDisconnectedEvent",
5219
5424
  "review.created": "#/components/schemas/ReviewCreatedEvent",
5220
5425
  "review.responded": "#/components/schemas/ReviewRespondedEvent",
@@ -5242,9 +5447,15 @@
5242
5447
  },
5243
5448
  "name": {
5244
5449
  "type": "string",
5245
- "description": "Listing title",
5450
+ "description": "The host's internal nickname for the listing.",
5246
5451
  "example": "Oceanview Villa"
5247
5452
  },
5453
+ "publicName": {
5454
+ "type": "string",
5455
+ "nullable": true,
5456
+ "description": "The title guests see on the channel (e.g. the Airbnb listing title). `name` is the host's internal nickname for the listing; show `publicName` in anything a guest or end user reads. Present on inactive rows too.",
5457
+ "example": "Centre Oxford bright single room D"
5458
+ },
5248
5459
  "city": {
5249
5460
  "type": "string",
5250
5461
  "nullable": true,
@@ -5446,7 +5657,30 @@
5446
5657
  },
5447
5658
  "name": {
5448
5659
  "type": "string",
5449
- "nullable": true
5660
+ "nullable": true,
5661
+ "description": "The host's internal nickname for the listing."
5662
+ },
5663
+ "publicName": {
5664
+ "type": "string",
5665
+ "nullable": true,
5666
+ "description": "The title guests see on the channel; show this to end users."
5667
+ },
5668
+ "status": {
5669
+ "type": "string",
5670
+ "enum": [
5671
+ "active",
5672
+ "inactive"
5673
+ ],
5674
+ "description": "Inactive listings appear only with `?status=inactive|all`, with identity fields only."
5675
+ },
5676
+ "inactiveReason": {
5677
+ "type": "string",
5678
+ "enum": [
5679
+ "plan_limit",
5680
+ "unlisted_on_airbnb",
5681
+ "deactivated"
5682
+ ],
5683
+ "description": "On inactive listings only: why it is inactive."
5450
5684
  },
5451
5685
  "city": {
5452
5686
  "type": "string",
@@ -6419,6 +6653,28 @@
6419
6653
  "4118"
6420
6654
  ]
6421
6655
  },
6656
+ "listings": {
6657
+ "type": "array",
6658
+ "description": "The same inactive listings with their names, so you can show the user which ones to activate. Present on `code: \"listing_inactive\"` (HTTP 403). `name` is null only when the request did not resolve it.",
6659
+ "items": {
6660
+ "type": "object",
6661
+ "required": [
6662
+ "id",
6663
+ "name"
6664
+ ],
6665
+ "properties": {
6666
+ "id": {
6667
+ "type": "string",
6668
+ "example": "4118"
6669
+ },
6670
+ "name": {
6671
+ "type": "string",
6672
+ "nullable": true,
6673
+ "example": "R-Sable 1302"
6674
+ }
6675
+ }
6676
+ }
6677
+ },
6422
6678
  "listing_id": {
6423
6679
  "type": "string",
6424
6680
  "description": "The single Repull listing the error is about. Present on `code: \"listing_not_api_connected\"` (HTTP 403).",
@@ -8488,8 +8744,17 @@
8488
8744
  },
8489
8745
  "Listing": {
8490
8746
  "type": "object",
8491
- "description": "A vacation rental listing in your Repull workspace.\n\nAn **inactive** listing appears only in `GET /v1/listings`, and only when `?status=` asks for it. Such a row carries identity fields only \u2014 `id`, `name`, `status`, `channels` \u2014 so `address`, `content`, `details`, `createdAt` and `updatedAt` are absent until the listing is activated. `GET /v1/listings/{id}` and every other listing endpoint answer `403 listing_inactive` for it. The one field you can add back is `thumbnailUrl`, by passing `?include=thumbnail` \u2014 enough to render an activate/deactivate picker with pictures from a single request.",
8747
+ "description": "A vacation rental listing in your Repull workspace.\n\nAn **inactive** listing appears only in `GET /v1/listings`, and only when `?status=` asks for it. Such a row carries identity fields only \u2014 `id`, `name`, `status`, `inactiveReason`, `address.city`, `channels` \u2014 so the street, `content`, `details`, `createdAt` and `updatedAt` are absent until the listing is activated. `inactiveReason` is `plan_limit` (held back by the plan; activating needs a free slot or an upgrade), `unlisted_on_airbnb`, or `deactivated` (switched off by you). `GET /v1/listings/{id}` and every other listing endpoint answer `403 listing_inactive` for it. The one field you can add back is `thumbnailUrl`, by passing `?include=thumbnail` \u2014 enough to render an activate/deactivate picker with pictures from a single request.",
8492
8748
  "properties": {
8749
+ "capabilities": {
8750
+ "type": "object",
8751
+ "description": "`GET /v1/listings/{id}` only. What the API can do with this listing.",
8752
+ "properties": {
8753
+ "reservations": {
8754
+ "$ref": "#/components/schemas/ReservationCapabilities"
8755
+ }
8756
+ }
8757
+ },
8493
8758
  "units": {
8494
8759
  "type": "array",
8495
8760
  "description": "`GET /v1/listings/{id}` only. The physical rooms under a hotel-model listing (a Mews or Cloudbeds room type); empty for a single home. Same items as `GET /v1/listings/{id}/units`.",
@@ -8529,8 +8794,15 @@
8529
8794
  },
8530
8795
  "name": {
8531
8796
  "type": "string",
8797
+ "description": "The host's internal nickname for the listing.",
8532
8798
  "example": "I - Stafford Apartment"
8533
8799
  },
8800
+ "publicName": {
8801
+ "type": "string",
8802
+ "nullable": true,
8803
+ "description": "The title guests see on the channel (e.g. the Airbnb listing title). `name` is the host's internal nickname for the listing; show `publicName` in anything a guest or end user reads. Present on inactive rows too.",
8804
+ "example": "Centre Oxford bright single room D"
8805
+ },
8534
8806
  "address": {
8535
8807
  "type": "object",
8536
8808
  "properties": {
@@ -10183,7 +10455,64 @@
10183
10455
  "type": "object",
10184
10456
  "additionalProperties": true,
10185
10457
  "nullable": true,
10186
- "description": "Required for `type: \"standard\" | \"rate-plan\" | \"fees\"` \u2014 the pricing-settings object to PUT."
10458
+ "description": "Required for `type: \"standard\" | \"rate-plan\"` \u2014 the pricing-settings object to PUT. With `type: \"fees\"` it is the raw alternative to `fees`: `{\"standard_fees\": [...]}` **replaces every fee** on the listing (Airbnb does not merge), so send the complete list. Prefer `fees`."
10459
+ },
10460
+ "fees": {
10461
+ "type": "array",
10462
+ "minItems": 1,
10463
+ "nullable": true,
10464
+ "description": "With `type: \"fees\"` \u2014 the fee changes to apply. **Merged by `fee_type`**: fees you do not mention are kept, the ones you send are set, and `amount: null` removes that fee. (Airbnb itself replaces the whole fee list on every write, so Repull reads the listing's current fees, applies your changes and writes the full set.) The response is the listing's fees as Airbnb holds them afterwards.\n\n**Units \u2014 the same as `GET \u2026/pricing` returns:** a `flat` fee is the amount in the listing currency \u00d7 1,000,000 (`160000000` = 160.00); a `percent` fee is a whole percent of the rent (`10` = 10%).\n\nExample \u2014 add a 10% management fee and keep everything else: `{\"type\":\"fees\",\"fees\":[{\"fee_type\":\"PASS_THROUGH_MANAGEMENT_FEE\",\"amount\":10,\"amount_type\":\"percent\"}]}`. Remove the pet fee: `{\"type\":\"fees\",\"fees\":[{\"fee_type\":\"PASS_THROUGH_PET_FEE\",\"amount\":null}]}`.",
10465
+ "items": {
10466
+ "type": "object",
10467
+ "required": [
10468
+ "fee_type",
10469
+ "amount"
10470
+ ],
10471
+ "additionalProperties": false,
10472
+ "properties": {
10473
+ "fee_type": {
10474
+ "type": "string",
10475
+ "description": "Airbnb fee type: `PASS_THROUGH_CLEANING_FEE`, `PASS_THROUGH_SHORT_TERM_CLEANING_FEE`, `PASS_THROUGH_PET_FEE`, `PASS_THROUGH_SECURITY_DEPOSIT`, `PASS_THROUGH_MANAGEMENT_FEE`, `PASS_THROUGH_RESORT_FEE`, `PASS_THROUGH_COMMUNITY_FEE`, `PASS_THROUGH_LINEN_FEE`.",
10476
+ "example": "PASS_THROUGH_MANAGEMENT_FEE"
10477
+ },
10478
+ "amount": {
10479
+ "type": "number",
10480
+ "nullable": true,
10481
+ "minimum": 0,
10482
+ "description": "`null` removes the fee. Flat: currency \u00d7 1,000,000. Percent: whole percent.",
10483
+ "example": 10
10484
+ },
10485
+ "amount_type": {
10486
+ "type": "string",
10487
+ "enum": [
10488
+ "flat",
10489
+ "percent"
10490
+ ],
10491
+ "description": "Defaults to the existing fee's, else `flat`. Percent is accepted for management and resort fees."
10492
+ },
10493
+ "charge_type": {
10494
+ "type": "string",
10495
+ "enum": [
10496
+ "PER_GROUP",
10497
+ "PER_PERSON",
10498
+ "PER_PET"
10499
+ ],
10500
+ "description": "Who it is charged per. Defaults to the existing fee's, else `PER_GROUP`."
10501
+ },
10502
+ "charge_period": {
10503
+ "type": "string",
10504
+ "enum": [
10505
+ "PER_BOOKING",
10506
+ "PER_NIGHT"
10507
+ ],
10508
+ "description": "Once per booking or per night. Defaults to the existing fee's, else `PER_BOOKING`."
10509
+ },
10510
+ "offline": {
10511
+ "type": "boolean",
10512
+ "description": "Collected offline by the host rather than through Airbnb. Default `false`."
10513
+ }
10514
+ }
10515
+ }
10187
10516
  },
10188
10517
  "records": {
10189
10518
  "type": "array",
@@ -10908,6 +11237,8 @@
10908
11237
  "checkOut",
10909
11238
  "guest"
10910
11239
  ],
11240
+ "additionalProperties": false,
11241
+ "description": "Which fields a listing takes depends on whether it is managed in a PMS \u2014 see the operation description and `GET /v1/listings/{id}` \u2192 `capabilities.reservations`. A field the listing cannot take is refused by name (`422 unsupported_field`), never dropped.",
10911
11242
  "properties": {
10912
11243
  "listingId": {
10913
11244
  "type": "integer",
@@ -10931,37 +11262,73 @@
10931
11262
  "platform": {
10932
11263
  "type": "string",
10933
11264
  "default": "direct",
10934
- "description": "OTA platforms are deliberately absent \u2014 those reservations are owned by the channel and arrive through sync."
11265
+ "description": "OTA platforms are deliberately absent \u2014 those reservations are owned by the channel and arrive through sync. `owner` is refused on a PMS listing (block owner stays in the PMS)."
10935
11266
  },
10936
11267
  "status": {
10937
11268
  "type": "string",
10938
- "default": "accept",
10939
- "description": "Lifecycle status to open the reservation in. Defaults to confirmed."
11269
+ "default": "confirmed",
11270
+ "description": "`confirmed` (default) or `tentative` (an optional hold, where the PMS has one). On a listing not managed in a PMS the value is passed to the reservation pipeline as before."
11271
+ },
11272
+ "adults": {
11273
+ "type": "integer",
11274
+ "minimum": 1,
11275
+ "example": 2
11276
+ },
11277
+ "children": {
11278
+ "type": "integer",
11279
+ "minimum": 0,
11280
+ "example": 1
11281
+ },
11282
+ "guestCount": {
11283
+ "type": "integer",
11284
+ "minimum": 1,
11285
+ "description": "Total guests. On a PMS listing without `adults`, used as the adult count.",
11286
+ "example": 3
11287
+ },
11288
+ "totalPrice": {
11289
+ "type": "number",
11290
+ "minimum": 0,
11291
+ "description": "PMS listings only: the total for the whole stay, in the listing's currency. Honoured where `capabilities.reservations.customPrice` is true; omit it and the PMS prices the stay (from its quote where it has one). Refused on a listing not managed in a PMS, whose rate engine prices the stay.",
11292
+ "example": 880
11293
+ },
11294
+ "notes": {
11295
+ "type": "string",
11296
+ "maxLength": 5000,
11297
+ "description": "PMS listings only: booking notes stored in the PMS.",
11298
+ "example": "Late arrival, around 22:00."
11299
+ },
11300
+ "unitId": {
11301
+ "type": "string",
11302
+ "description": "PMS listings only: book this unit (`GET /v1/listings/{id}` \u2192 `units[].id`). Refused by PMSs that cannot target a unit.",
11303
+ "example": "3f1c9a20"
11304
+ },
11305
+ "sendConfirmationEmail": {
11306
+ "type": "boolean",
11307
+ "description": "PMS listings only: ask the PMS to email the guest its own confirmation, where the PMS supports it.",
11308
+ "example": false
10940
11309
  },
10941
11310
  "checkInTime": {
10942
11311
  "type": "string",
10943
11312
  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
11313
+ "description": "Listings not managed in a PMS only.",
10944
11314
  "example": "16:00"
10945
11315
  },
10946
11316
  "checkOutTime": {
10947
11317
  "type": "string",
10948
11318
  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
11319
+ "description": "Listings not managed in a PMS only.",
10949
11320
  "example": "10:00"
10950
11321
  },
10951
11322
  "guestId": {
10952
11323
  "type": "integer",
10953
- "description": "Attach an existing guest instead of matching/creating one. Must belong to this workspace.",
11324
+ "description": "Listings not managed in a PMS only: attach an existing guest instead of matching/creating one. Must belong to this workspace.",
10954
11325
  "example": 91234
10955
11326
  },
10956
- "guestCount": {
10957
- "type": "integer",
10958
- "minimum": 1,
10959
- "example": 2
10960
- },
10961
11327
  "currency": {
10962
11328
  "type": "string",
10963
11329
  "minLength": 3,
10964
11330
  "maxLength": 3,
11331
+ "description": "Listings not managed in a PMS only (a PMS books in the property's currency).",
10965
11332
  "example": "USD"
10966
11333
  }
10967
11334
  }
@@ -10970,17 +11337,17 @@
10970
11337
  "type": "object",
10971
11338
  "properties": {
10972
11339
  "id": {
10973
- "type": "integer",
10974
- "description": "Pass to `GET /v1/reservations/{id}` for the full record.",
10975
- "example": 215708
11340
+ "type": "string",
11341
+ "description": "Pass to `GET /v1/reservations/{id}` for the full record. A string, like every id in API responses.",
11342
+ "example": "215708"
10976
11343
  },
10977
11344
  "confirmationCode": {
10978
11345
  "type": "string",
10979
11346
  "example": "DIR-8H2K4N"
10980
11347
  },
10981
11348
  "listingId": {
10982
- "type": "integer",
10983
- "example": 4118
11349
+ "type": "string",
11350
+ "example": "4118"
10984
11351
  },
10985
11352
  "platform": {
10986
11353
  "type": "string",
@@ -11000,13 +11367,14 @@
11000
11367
  "format": "date"
11001
11368
  },
11002
11369
  "guestId": {
11003
- "type": "integer",
11004
- "nullable": true
11370
+ "type": "string",
11371
+ "nullable": true,
11372
+ "example": "91234"
11005
11373
  },
11006
11374
  "totalPrice": {
11007
11375
  "type": "number",
11008
11376
  "nullable": true,
11009
- "description": "The price the pricing engine derived for the stay. Reservations created through this endpoint are NOT priced from the request \u2014 see the operation description. On a Mews or Cloudbeds listing, the PMS prices it from its own rate."
11377
+ "description": "The total the booking was recorded at. On a PMS listing: the PMS's total (your `totalPrice` where the PMS honours one, else the PMS's own price). On any other listing: the price the rate engine derived (`0` when the listing has no rates for the range)."
11010
11378
  },
11011
11379
  "currency": {
11012
11380
  "type": "string",
@@ -11015,7 +11383,7 @@
11015
11383
  "unit": {
11016
11384
  "type": "object",
11017
11385
  "nullable": true,
11018
- "description": "Mews or Cloudbeds listings only: the room the PMS assigned. Absent for every other listing.",
11386
+ "description": "PMS listings: the unit the PMS assigned (hotel-model PMSs), or null. Absent for direct bookings.",
11019
11387
  "properties": {
11020
11388
  "id": {
11021
11389
  "type": "string"
@@ -11027,41 +11395,7 @@
11027
11395
  }
11028
11396
  },
11029
11397
  "pms": {
11030
- "type": "object",
11031
- "description": "Mews or Cloudbeds listings only: the booking was made in the PMS first, and this is what it applied.",
11032
- "properties": {
11033
- "provider": {
11034
- "type": "string",
11035
- "example": "mews"
11036
- },
11037
- "reservationId": {
11038
- "type": "string",
11039
- "description": "The PMS's own id for the booking."
11040
- },
11041
- "applied": {
11042
- "type": "array",
11043
- "items": {
11044
- "type": "string"
11045
- }
11046
- },
11047
- "errors": {
11048
- "type": "array",
11049
- "items": {
11050
- "type": "object",
11051
- "properties": {
11052
- "section": {
11053
- "type": "string"
11054
- },
11055
- "message": {
11056
- "type": "string"
11057
- },
11058
- "code": {
11059
- "type": "string"
11060
- }
11061
- }
11062
- }
11063
- }
11064
- }
11398
+ "$ref": "#/components/schemas/ReservationPmsOutcome"
11065
11399
  }
11066
11400
  }
11067
11401
  },
@@ -11106,14 +11440,15 @@
11106
11440
  "type": "object",
11107
11441
  "properties": {
11108
11442
  "id": {
11109
- "type": "integer"
11443
+ "type": "string",
11444
+ "description": "A string, like every id in API responses."
11110
11445
  },
11111
11446
  "confirmationCode": {
11112
11447
  "type": "string",
11113
11448
  "nullable": true
11114
11449
  },
11115
11450
  "listingId": {
11116
- "type": "integer",
11451
+ "type": "string",
11117
11452
  "nullable": true
11118
11453
  },
11119
11454
  "checkIn": {
@@ -11153,6 +11488,240 @@
11153
11488
  "example": [
11154
11489
  "checkOut"
11155
11490
  ]
11491
+ },
11492
+ "pms": {
11493
+ "$ref": "#/components/schemas/ReservationPmsOutcome"
11494
+ }
11495
+ }
11496
+ },
11497
+ "ReservationPmsOutcome": {
11498
+ "type": "object",
11499
+ "description": "Present when the write was made in a PMS: what the PMS applied. `partial: true` means the booking exists in the PMS but the steps in `failedSections` (e.g. notes, a tentative state) did not apply \u2014 do not create it again.",
11500
+ "properties": {
11501
+ "provider": {
11502
+ "type": "string",
11503
+ "example": "hostaway"
11504
+ },
11505
+ "reservationId": {
11506
+ "type": "string",
11507
+ "nullable": true,
11508
+ "description": "The PMS's own id for the booking.",
11509
+ "example": "4471923"
11510
+ },
11511
+ "applied": {
11512
+ "type": "array",
11513
+ "items": {
11514
+ "type": "string"
11515
+ },
11516
+ "example": [
11517
+ "reservation"
11518
+ ]
11519
+ },
11520
+ "errors": {
11521
+ "type": "array",
11522
+ "items": {
11523
+ "$ref": "#/components/schemas/ReservationPmsSectionError"
11524
+ }
11525
+ },
11526
+ "partial": {
11527
+ "type": "boolean",
11528
+ "example": false
11529
+ },
11530
+ "failedSections": {
11531
+ "type": "array",
11532
+ "items": {
11533
+ "$ref": "#/components/schemas/ReservationPmsSectionError"
11534
+ }
11535
+ },
11536
+ "quote": {
11537
+ "type": "object",
11538
+ "description": "Create only: the PMS quote the booking was priced from, when no `totalPrice` was sent.",
11539
+ "properties": {
11540
+ "available": {
11541
+ "type": "boolean"
11542
+ },
11543
+ "total": {
11544
+ "type": "number",
11545
+ "nullable": true
11546
+ },
11547
+ "currency": {
11548
+ "type": "string",
11549
+ "nullable": true
11550
+ }
11551
+ }
11552
+ }
11553
+ }
11554
+ },
11555
+ "ReservationPmsSectionError": {
11556
+ "type": "object",
11557
+ "properties": {
11558
+ "section": {
11559
+ "type": "string",
11560
+ "example": "notes"
11561
+ },
11562
+ "code": {
11563
+ "type": "string",
11564
+ "example": "rejected"
11565
+ },
11566
+ "message": {
11567
+ "type": "string",
11568
+ "example": "Smoobu: notice too long"
11569
+ }
11570
+ }
11571
+ },
11572
+ "ReservationQuoteRequest": {
11573
+ "type": "object",
11574
+ "required": [
11575
+ "listingId",
11576
+ "checkIn",
11577
+ "checkOut"
11578
+ ],
11579
+ "additionalProperties": false,
11580
+ "properties": {
11581
+ "listingId": {
11582
+ "type": "integer",
11583
+ "example": 4118
11584
+ },
11585
+ "checkIn": {
11586
+ "type": "string",
11587
+ "format": "date",
11588
+ "example": "2026-10-01"
11589
+ },
11590
+ "checkOut": {
11591
+ "type": "string",
11592
+ "format": "date",
11593
+ "description": "Must be after `checkIn`.",
11594
+ "example": "2026-10-05"
11595
+ },
11596
+ "adults": {
11597
+ "type": "integer",
11598
+ "minimum": 1,
11599
+ "example": 2
11600
+ },
11601
+ "children": {
11602
+ "type": "integer",
11603
+ "minimum": 0,
11604
+ "example": 1
11605
+ },
11606
+ "guestCount": {
11607
+ "type": "integer",
11608
+ "minimum": 1,
11609
+ "description": "Total guests, when you do not split adults and children.",
11610
+ "example": 3
11611
+ },
11612
+ "unitId": {
11613
+ "type": "string",
11614
+ "description": "Quote one unit (`GET /v1/listings/{id}` \u2192 `units[].id`).",
11615
+ "example": "3f1c9a20"
11616
+ }
11617
+ }
11618
+ },
11619
+ "ReservationQuoteResponse": {
11620
+ "type": "object",
11621
+ "properties": {
11622
+ "listingId": {
11623
+ "type": "string",
11624
+ "example": "4118"
11625
+ },
11626
+ "provider": {
11627
+ "type": "string",
11628
+ "description": "The PMS that priced it.",
11629
+ "example": "hostaway"
11630
+ },
11631
+ "checkIn": {
11632
+ "type": "string",
11633
+ "format": "date"
11634
+ },
11635
+ "checkOut": {
11636
+ "type": "string",
11637
+ "format": "date"
11638
+ },
11639
+ "available": {
11640
+ "type": "boolean",
11641
+ "description": "Whether the PMS would take the booking as asked.",
11642
+ "example": true
11643
+ },
11644
+ "total": {
11645
+ "type": "number",
11646
+ "nullable": true,
11647
+ "description": "Total for the stay, in `currency`. Null when the PMS gave no price (e.g. not available).",
11648
+ "example": 880
11649
+ },
11650
+ "currency": {
11651
+ "type": "string",
11652
+ "nullable": true,
11653
+ "example": "USD"
11654
+ },
11655
+ "breakdown": {
11656
+ "type": "object",
11657
+ "nullable": true,
11658
+ "description": "The parts the PMS itemized; absent parts were not itemized.",
11659
+ "properties": {
11660
+ "accommodation": {
11661
+ "type": "number"
11662
+ },
11663
+ "cleaningFee": {
11664
+ "type": "number"
11665
+ },
11666
+ "taxes": {
11667
+ "type": "number"
11668
+ },
11669
+ "fees": {
11670
+ "type": "number"
11671
+ }
11672
+ }
11673
+ },
11674
+ "restrictions": {
11675
+ "type": "array",
11676
+ "items": {
11677
+ "type": "string"
11678
+ },
11679
+ "description": "The PMS's reasons, verbatim, when `available` is false.",
11680
+ "example": []
11681
+ }
11682
+ }
11683
+ },
11684
+ "ReservationCapabilities": {
11685
+ "type": "object",
11686
+ "description": "Which reservation writes the API performs for this listing (or, on `GET /v1/connect/{provider}`, for any listing of that connection). Derived from the PMS connector, the connection, and its write policy \u2014 a flag is true only when all three allow it.",
11687
+ "properties": {
11688
+ "managedBy": {
11689
+ "type": "string",
11690
+ "description": "`pms` \u2014 booked in the connected PMS; `repull` \u2014 a direct booking made in Repull."
11691
+ },
11692
+ "provider": {
11693
+ "type": "string",
11694
+ "nullable": true,
11695
+ "example": "hostaway"
11696
+ },
11697
+ "create": {
11698
+ "type": "boolean",
11699
+ "description": "`POST /v1/reservations`."
11700
+ },
11701
+ "modify": {
11702
+ "type": "boolean",
11703
+ "description": "`PATCH /v1/reservations/{id}`."
11704
+ },
11705
+ "cancel": {
11706
+ "type": "boolean",
11707
+ "description": "`POST /v1/reservations/{id}/cancel`."
11708
+ },
11709
+ "quote": {
11710
+ "type": "boolean",
11711
+ "description": "`POST /v1/reservations/quote`."
11712
+ },
11713
+ "customPrice": {
11714
+ "type": "boolean",
11715
+ "description": "`totalPrice` on create is honoured; otherwise the PMS (or the rate engine) prices the stay."
11716
+ },
11717
+ "notes": {
11718
+ "type": "string",
11719
+ "description": "What the flags do not say: limits, required access, and why something is off."
11720
+ },
11721
+ "verifiedAgainst": {
11722
+ "type": "string",
11723
+ "nullable": true,
11724
+ "description": "`sandbox` \u2014 run end to end on the vendor sandbox (Mews, Cloudbeds); `vendor_docs` \u2014 verified against the vendor's API documentation only. Null for direct bookings."
11156
11725
  }
11157
11726
  }
11158
11727
  },
@@ -12612,31 +13181,13 @@
12612
13181
  "answers": {
12613
13182
  "type": "object",
12614
13183
  "minProperties": 1,
12615
- "description": "Keyed by each question's `answer_key`. Each value carries exactly one field, chosen by the question's `type`: TEXT \u2192 `text_value`, ATTESTATION \u2192 `attestation_value`, RADIO \u2192 `radio_value`, DATE \u2192 `date_value`, SELECT \u2192 `selected_options_value`.",
13184
+ "description": "Keyed by each question's `answer_key`. Each value carries exactly one `<type>_value` field named after the question's `type` (lower-case): `text_value`, `attestation_value` (boolean), `radio_value`, `dropdown_value`, `email_value`, `future_date_value` (YYYY-MM-DD) and `file_upload_value` (object with the base64 file) are the ones Airbnb returns in production; other question types follow the same pattern. Airbnb validates the value against its question. Example: `{\"email\": {\"email_value\": \"host@example.com\"}, \"expiration_date\": {\"future_date_value\": \"2029-02-04\"}, \"attestation\": {\"attestation_value\": true}}`.",
12616
13185
  "additionalProperties": {
12617
13186
  "type": "object",
12618
- "additionalProperties": false,
12619
- "properties": {
12620
- "text_value": {
12621
- "type": "string"
12622
- },
12623
- "attestation_value": {
12624
- "type": "boolean"
12625
- },
12626
- "radio_value": {
12627
- "type": "string"
12628
- },
12629
- "date_value": {
12630
- "type": "string",
12631
- "description": "ISO date, YYYY-MM-DD."
12632
- },
12633
- "selected_options_value": {
12634
- "type": "array",
12635
- "items": {
12636
- "type": "string"
12637
- }
12638
- }
12639
- }
13187
+ "minProperties": 1,
13188
+ "maxProperties": 1,
13189
+ "description": "Exactly one `<type>_value` field.",
13190
+ "additionalProperties": true
12640
13191
  }
12641
13192
  }
12642
13193
  }
@@ -12720,7 +13271,7 @@
12720
13271
  },
12721
13272
  "AirbnbListingDetailsWriteRequest": {
12722
13273
  "type": "object",
12723
- "description": "Update what kind of property this is, when the quiet hours are, or how the guest gets in. At least one field required. These are among the attributes Airbnb locks on established listings \u2014 see `blockedFields` on the response.",
13274
+ "description": "Update what kind of property this is, when the quiet hours are, how the guest gets in, the house manual, directions or Wi-Fi details. At least one field required. These are among the attributes Airbnb locks on established listings \u2014 see `blockedFields` on the response.",
12724
13275
  "additionalProperties": false,
12725
13276
  "properties": {
12726
13277
  "property_type_group": {
@@ -12764,7 +13315,7 @@
12764
13315
  "category"
12765
13316
  ],
12766
13317
  "additionalProperties": false,
12767
- "description": "How the guest lets themselves in \u2014 Airbnb's `check_in_option`.",
13318
+ "description": "How the guest lets themselves in \u2014 Airbnb's `check_in_option`. `instruction` is the arrival instructions the guest sees.",
12768
13319
  "properties": {
12769
13320
  "category": {
12770
13321
  "type": "string",
@@ -12783,6 +13334,30 @@
12783
13334
  "maxLength": 2000
12784
13335
  }
12785
13336
  }
13337
+ },
13338
+ "house_manual": {
13339
+ "type": "string",
13340
+ "nullable": true,
13341
+ "maxLength": 10000,
13342
+ "description": "The house manual guests see after booking."
13343
+ },
13344
+ "directions": {
13345
+ "type": "string",
13346
+ "nullable": true,
13347
+ "maxLength": 5000,
13348
+ "description": "Directions to the property, shown to booked guests."
13349
+ },
13350
+ "wifi_network": {
13351
+ "type": "string",
13352
+ "nullable": true,
13353
+ "maxLength": 255,
13354
+ "description": "Wi-Fi network name."
13355
+ },
13356
+ "wifi_password": {
13357
+ "type": "string",
13358
+ "nullable": true,
13359
+ "maxLength": 255,
13360
+ "description": "Wi-Fi password."
12786
13361
  }
12787
13362
  }
12788
13363
  },
@@ -12935,6 +13510,16 @@
12935
13510
  "example": "2026-08-01T00:00:00Z"
12936
13511
  }
12937
13512
  },
13513
+ "Account": {
13514
+ "name": "account",
13515
+ "in": "query",
13516
+ "required": false,
13517
+ "schema": {
13518
+ "type": "string",
13519
+ "example": "airbnb:79730216"
13520
+ },
13521
+ "description": "Only the records of one connected account, as `provider:externalAccountId` \u2014 the pair from a webhook `account` block, `connect.session.completed`, or `GET /v1/connect/{provider}` \u2192 `accounts`. A record belongs to an account when it is on that account's listings and on its channel (a PMS account: came in through that PMS). An account with no listings returns an empty page."
13522
+ },
12938
13523
  "Offset": {
12939
13524
  "name": "offset",
12940
13525
  "in": "query",
@@ -13052,6 +13637,12 @@
13052
13637
  "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
13053
13638
  "listing_ids": [
13054
13639
  "4118"
13640
+ ],
13641
+ "listings": [
13642
+ {
13643
+ "id": "4118",
13644
+ "name": "R-Sable 1302"
13645
+ }
13055
13646
  ]
13056
13647
  }
13057
13648
  }
@@ -13546,6 +14137,9 @@
13546
14137
  "Properties"
13547
14138
  ],
13548
14139
  "parameters": [
14140
+ {
14141
+ "$ref": "#/components/parameters/Account"
14142
+ },
13549
14143
  {
13550
14144
  "name": "limit",
13551
14145
  "in": "query",
@@ -13588,7 +14182,7 @@
13588
14182
  ],
13589
14183
  "default": "active"
13590
14184
  },
13591
- "description": "Filter by status. Default returns active only; pass `inactive` to invert or `all` to include both. Inactive properties carry identity fields only \u2014 `id`, `name`, `status`, `lifecycleStatus`, `channels` and `updatedAt` \u2014 never `address`, `city` or `currency`."
14185
+ "description": "Filter by status. Default returns active only; pass `inactive` to invert or `all` to include both. Inactive properties carry identity fields only \u2014 `id`, `name`, `city`, `status`, `inactiveReason` (`plan_limit`, `unlisted_on_airbnb` or `deactivated`), `lifecycleStatus`, `channels` and `updatedAt` \u2014 never `address` or `currency`."
13592
14186
  },
13593
14187
  {
13594
14188
  "name": "lifecycle_status",
@@ -13816,6 +14410,9 @@
13816
14410
  "Reservations"
13817
14411
  ],
13818
14412
  "parameters": [
14413
+ {
14414
+ "$ref": "#/components/parameters/Account"
14415
+ },
13819
14416
  {
13820
14417
  "$ref": "#/components/parameters/XSchemaHeader"
13821
14418
  },
@@ -14006,13 +14603,23 @@
14006
14603
  "post": {
14007
14604
  "operationId": "create_reservation",
14008
14605
  "summary": "Create a reservation",
14009
- "description": "Creates a reservation and everything that hangs off one: the guest, the conversation thread, the dashboard item, the calendar block, and the `reservation.created` fan-out that issues the door code and starts the messaging automations.\n\n**Platform is restricted to `direct`, `website` and `owner`.** Reservations on Airbnb, Booking.com and Vrbo are owned by the channel and arrive through sync \u2014 creating one here would mint a local booking the channel has never heard of, which then fights the next sync. Create those on the channel.\n\n**Dates are validated** (`YYYY-MM-DD`, and `checkOut` must be after `checkIn`), and an unrecognised field is rejected by name rather than silently ignored.\n\n**This endpoint does not set the price.** There is no `totalPrice` field: the reservation pipeline derives the price breakdown from the property's own rates and overwrites anything supplied, so accepting a total would be taking a value and discarding it. A reservation created here is priced by that engine (`0` when the property has no rates for the range). `currency` IS honoured. Quote a stay with `GET /v1/quotes` before booking if you need the figure up front.\n\n**Availability is NOT checked.** This creates the reservation you asked for even if the dates overlap an existing booking. Call `GET /v1/availability/{propertyId}` first if that matters.\n\nSend `Idempotency-Key` \u2014 a network timeout here is exactly the case it exists for: without it, a retry books the guest twice.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
14606
+ "description": "Creates a reservation \u2014 in the listing's PMS when it has one, otherwise as a direct booking in Repull.\n\n### Where the booking is made\n\n- **A listing managed in a connected PMS** (Mews, Cloudbeds, Hostaway, Guesty, Beds24, BookingSync, Lodgify, Smoobu, Hospitable, iGMS, OwnerRez): the booking is created **in the PMS first**, then recorded in Repull from the PMS's own record, so the next sync lands on the same confirmation code and nothing is duplicated. A booking is **never** created only in Repull for such a listing \u2014 the PMS would keep selling the dates. What the PMS cannot do is refused (`422 pms_write_unsupported`), never faked. The PMS checks availability: taken dates answer `409 pms_unavailable`.\n- **Any other listing**: a direct booking made in Repull, with everything that hangs off one \u2014 the guest, the conversation, the calendar block and the `reservation.created` fan-out that issues the door code and starts the messaging automations. Priced by the listing's own rates; **availability is NOT checked** (call `GET /v1/availability/{propertyId}` first if that matters).\n\n`GET /v1/listings/{id}` \u2192 `capabilities.reservations` says which applies to a listing and exactly what it supports (`create`, `modify`, `cancel`, `quote`, `customPrice`, plus `notes`).\n\n### Fields by listing kind\n\n| Field | PMS listing | Direct-booking listing |\n|---|---|---|\n| `listingId`, `checkIn`, `checkOut`, `guest`, `guestCount`, `adults`, `children` | \u2713 | \u2713 |\n| `status` | `confirmed` (default) or `tentative` | \u2713 |\n| `totalPrice` | \u2713 where `capabilities.reservations.customPrice`; otherwise the PMS prices the stay | `422 unsupported_field` (priced from the listing's rates) |\n| `notes`, `unitId`, `sendConfirmationEmail` | \u2713 | `422 unsupported_field` |\n| `checkInTime`, `checkOutTime`, `currency`, `guestId` | `422 unsupported_field` (the PMS's own settings apply) | \u2713 |\n| `platform` | `direct` or `website` (`owner` \u2192 `422 pms_write_unsupported`; block owner stays in the PMS) | `direct`, `website` or `owner` |\n\nA field a listing cannot take is refused by name, never silently dropped. `platform` never accepts `airbnb` / `booking` / `vrbo`: those reservations are owned by the channel and arrive through sync.\n\n### Per-PMS limits\n\n| PMS | create | change | cancel | quote | `totalPrice` | Limits |\n|---|---|---|---|---|---|---|\n| Mews | \u2713 | \u2713 | \u2713 | \u2013 | \u2713 | \u2014 |\n| Cloudbeds | \u2713 | \u2713 | \u2713 | \u2013 | \u2013 | Books at the rate plan's price; group bookings supported. |\n| Hostaway | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Direct-channel bookings only; a specific unit is refused; a date change keeps the booked total. |\n| Guesty | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Cancels direct and Vrbo bookings; other channel bookings are cancelled on the channel. |\n| Beds24 | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Needs the `write:bookings` scope; a multi-room property needs `unitId`. |\n| BookingSync | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Needs `bookings_write`; fees and taxes are not itemized; no guest email. |\n| Lodgify | \u2713 | \u2713 | \u2713 (declines) | \u2713 | \u2713 | Cancel declines the booking; single-room bookings. |\n| Smoobu | \u2713 | \u2713 (no dates) | \u2713 | \u2713 | \u2713 | Dates cannot be changed through Smoobu's API \u2014 cancel and rebook, or change them in Smoobu. |\n| Hospitable | \u2713 | \u2713 | \u2713 | \u2713 (Direct plan) | \u2713 | Manual reservations only; needs `reservation:write`; adds no fees or taxes. |\n| iGMS | \u2713 | \u2713 | \u2713 | \u2013 | \u2713 (required) | iGMS direct bookings only; a price is required; no tentative holds. |\n| OwnerRez | \u2713 | \u2713 | \u2013 | \u2713 | \u2013 | No cancel through OwnerRez's API; priced by the property's own rates; needs the `full` scope. |\n\nEvery PMS except Cloudbeds refuses group bookings, and every vacation-rental PMS refuses to change or cancel a booking that came from a channel (Airbnb, Booking.com, Vrbo\u2026) \u2014 that is done on the channel.\n\n**Verification.** Mews and Cloudbeds were run end to end on their vendors' sandboxes. Every other PMS is **verified against the vendor's API documentation only** \u2014 no live account has been written to yet. `capabilities.reservations.verifiedAgainst` says which.\n\n### Idempotency\n\n**Send `Idempotency-Key`.** A network timeout here is exactly the case it exists for. The key is also sent to the PMS as the booking's reference, so even a retry that reaches the PMS again finds the booking instead of making a second one (`409 pms_duplicate` with `existing`, or the existing booking returned). A completed answer is replayed with `Idempotency-Status: cached` and the PMS is not called again. `502 pms_error` (the PMS could not be reached) is NOT stored \u2014 retry with the same key. `502 reservation_created_in_pms_only` IS stored, unlike every other 5xx: the booking exists in the PMS and arrives with the next sync, so a retry replays that answer rather than booking twice.\n\n### Partial success\n\nWhen the PMS created the booking but a follow-up step did not apply (for example the notes, or a tentative state), the response is still `201`, with `pms.partial: true` and the steps in `pms.failedSections`. The booking exists \u2014 do not create it again.\n\n`X-Account-Id` restricts the listing to one connected account. Returns `403 listing_inactive` when the listing is inactive.",
14010
14607
  "tags": [
14011
14608
  "Reservations"
14012
14609
  ],
14013
14610
  "parameters": [
14014
14611
  {
14015
14612
  "$ref": "#/components/parameters/IdempotencyKey"
14613
+ },
14614
+ {
14615
+ "name": "X-Account-Id",
14616
+ "in": "header",
14617
+ "required": false,
14618
+ "schema": {
14619
+ "type": "string",
14620
+ "example": "126"
14621
+ },
14622
+ "description": "Restrict the request to one connected account (a Repull connection id, `GET /v1/connect` \u2192 `id`). A listing or reservation outside that account answers `404 not_found`. Omit it to act workspace-wide."
14016
14623
  }
14017
14624
  ],
14018
14625
  "requestBody": {
@@ -14021,17 +14628,159 @@
14021
14628
  "application/json": {
14022
14629
  "schema": {
14023
14630
  "$ref": "#/components/schemas/ReservationCreateRequest"
14631
+ },
14632
+ "examples": {
14633
+ "pms": {
14634
+ "summary": "A listing managed in a PMS, at a set price",
14635
+ "value": {
14636
+ "listingId": 4118,
14637
+ "checkIn": "2026-10-01",
14638
+ "checkOut": "2026-10-05",
14639
+ "guest": {
14640
+ "firstName": "Ada",
14641
+ "lastName": "Lovelace",
14642
+ "email": "ada@example.com",
14643
+ "phone": "+14035551234"
14644
+ },
14645
+ "adults": 2,
14646
+ "children": 1,
14647
+ "totalPrice": 880,
14648
+ "notes": "Late arrival, around 22:00.",
14649
+ "status": "confirmed",
14650
+ "sendConfirmationEmail": false
14651
+ }
14652
+ },
14653
+ "direct": {
14654
+ "summary": "A direct booking on a listing no PMS manages",
14655
+ "value": {
14656
+ "listingId": 4119,
14657
+ "checkIn": "2026-10-01",
14658
+ "checkOut": "2026-10-05",
14659
+ "guest": {
14660
+ "firstName": "Ada",
14661
+ "email": "ada@example.com"
14662
+ },
14663
+ "guestCount": 2,
14664
+ "checkInTime": "16:00",
14665
+ "currency": "USD"
14666
+ }
14667
+ }
14024
14668
  }
14025
14669
  }
14026
14670
  }
14027
14671
  },
14028
14672
  "responses": {
14029
14673
  "201": {
14030
- "description": "Reservation created",
14674
+ "description": "Reservation created. `pms` is present when it was booked in a PMS; `pms.partial: true` means the booking exists but `pms.failedSections` did not apply.",
14031
14675
  "content": {
14032
14676
  "application/json": {
14033
14677
  "schema": {
14034
14678
  "$ref": "#/components/schemas/ReservationCreateResponse"
14679
+ },
14680
+ "examples": {
14681
+ "pms": {
14682
+ "summary": "Booked in Hostaway",
14683
+ "value": {
14684
+ "id": "215708",
14685
+ "confirmationCode": "HA-4471923",
14686
+ "listingId": "4118",
14687
+ "platform": "direct",
14688
+ "status": "confirmed",
14689
+ "checkIn": "2026-10-01",
14690
+ "checkOut": "2026-10-05",
14691
+ "guestId": "91234",
14692
+ "totalPrice": 880,
14693
+ "currency": "USD",
14694
+ "unit": null,
14695
+ "pms": {
14696
+ "provider": "hostaway",
14697
+ "reservationId": "4471923",
14698
+ "applied": [
14699
+ "reservation"
14700
+ ],
14701
+ "errors": [],
14702
+ "partial": false,
14703
+ "failedSections": []
14704
+ }
14705
+ }
14706
+ },
14707
+ "partial": {
14708
+ "summary": "Booked in Smoobu; the notes did not apply",
14709
+ "value": {
14710
+ "id": "215709",
14711
+ "confirmationCode": "SM-1290031",
14712
+ "listingId": "4120",
14713
+ "platform": "direct",
14714
+ "status": "confirmed",
14715
+ "checkIn": "2026-10-01",
14716
+ "checkOut": "2026-10-05",
14717
+ "guestId": "91235",
14718
+ "totalPrice": 640,
14719
+ "currency": "EUR",
14720
+ "unit": null,
14721
+ "pms": {
14722
+ "provider": "smoobu",
14723
+ "reservationId": "1290031",
14724
+ "applied": [
14725
+ "reservation"
14726
+ ],
14727
+ "errors": [
14728
+ {
14729
+ "section": "notes",
14730
+ "code": "rejected",
14731
+ "message": "Smoobu: notice too long"
14732
+ }
14733
+ ],
14734
+ "partial": true,
14735
+ "failedSections": [
14736
+ {
14737
+ "section": "notes",
14738
+ "code": "rejected",
14739
+ "message": "Smoobu: notice too long"
14740
+ }
14741
+ ]
14742
+ }
14743
+ }
14744
+ },
14745
+ "direct": {
14746
+ "summary": "A direct booking",
14747
+ "value": {
14748
+ "id": "215710",
14749
+ "confirmationCode": "DIR-8H2K4N",
14750
+ "listingId": "4119",
14751
+ "platform": "direct",
14752
+ "status": "confirmed",
14753
+ "checkIn": "2026-10-01",
14754
+ "checkOut": "2026-10-05",
14755
+ "guestId": "91236",
14756
+ "totalPrice": 0,
14757
+ "currency": "USD"
14758
+ }
14759
+ }
14760
+ }
14761
+ }
14762
+ }
14763
+ },
14764
+ "400": {
14765
+ "description": "The body is not JSON.",
14766
+ "content": {
14767
+ "application/json": {
14768
+ "schema": {
14769
+ "$ref": "#/components/schemas/Error"
14770
+ },
14771
+ "examples": {
14772
+ "invalidJson": {
14773
+ "summary": "The body is not JSON",
14774
+ "value": {
14775
+ "error": {
14776
+ "code": "invalid_json",
14777
+ "message": "Request body is not valid JSON: Unexpected token",
14778
+ "fix": "Send a JSON object with `Content-Type: application/json`.",
14779
+ "docs_url": "https://repull.dev/docs/errors/invalid_json",
14780
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
14781
+ }
14782
+ }
14783
+ }
14035
14784
  }
14036
14785
  }
14037
14786
  }
@@ -14040,23 +14789,295 @@
14040
14789
  "$ref": "#/components/responses/Unauthorized"
14041
14790
  },
14042
14791
  "403": {
14043
- "$ref": "#/components/responses/ListingInactive"
14792
+ "description": "The listing is inactive (`listing_inactive`), or the PMS connection lacks write access to bookings (`connection_reauth_required` \u2014 reconnect, then retry with the same key).",
14793
+ "content": {
14794
+ "application/json": {
14795
+ "schema": {
14796
+ "$ref": "#/components/schemas/Error"
14797
+ },
14798
+ "examples": {
14799
+ "reauthRequired": {
14800
+ "summary": "The grant lacks write access to bookings",
14801
+ "value": {
14802
+ "error": {
14803
+ "code": "connection_reauth_required",
14804
+ "message": "Beds24 needs to be reconnected before Repull can create reservations there: Beds24: token lacks scope write:bookings",
14805
+ "fix": "Reconnect Beds24 with an invite code that grants `write:bookings` (plus `bookings-personal` and `bookings-financial`), then retry.",
14806
+ "docs_url": "https://repull.dev/docs/errors/connection_reauth_required",
14807
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14808
+ "provider": "beds24",
14809
+ "reconnect": {
14810
+ "method": "POST",
14811
+ "path": "/v1/connect/beds24"
14812
+ }
14813
+ }
14814
+ }
14815
+ },
14816
+ "listingInactive": {
14817
+ "summary": "The listing is inactive",
14818
+ "value": {
14819
+ "error": {
14820
+ "code": "listing_inactive",
14821
+ "message": "Listing 4118 is inactive.",
14822
+ "fix": "Activate it with `PATCH /v1/listings/{id}` `{\"active\": true}`, then retry.",
14823
+ "docs_url": "https://repull.dev/docs/errors/listing_inactive",
14824
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
14825
+ }
14826
+ }
14827
+ }
14828
+ }
14829
+ }
14830
+ }
14044
14831
  },
14045
14832
  "404": {
14046
- "description": "The property, or the supplied `guestId`, does not exist in this workspace.",
14833
+ "description": "The listing (or `guestId`) is not in this workspace, or not in the `X-Account-Id` account.",
14834
+ "content": {
14835
+ "application/json": {
14836
+ "schema": {
14837
+ "$ref": "#/components/schemas/Error"
14838
+ },
14839
+ "examples": {
14840
+ "notFound": {
14841
+ "summary": "Not in this workspace (or not in the X-Account-Id account)",
14842
+ "value": {
14843
+ "error": {
14844
+ "code": "not_found",
14845
+ "message": "Listing 4118 not found in this workspace.",
14846
+ "fix": "Verify the listing exists in this workspace (`GET /v1/listings`) and, with `X-Account-Id`, that it belongs to that account.",
14847
+ "docs_url": "https://repull.dev/docs/errors/not_found",
14848
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
14849
+ }
14850
+ }
14851
+ }
14852
+ }
14853
+ }
14854
+ }
14855
+ },
14856
+ "409": {
14857
+ "description": "The PMS will not take it as asked: writes off for the API (`pms_writes_off`), no connection (`no_connection`), dates taken (`pms_unavailable`), or already booked with this key (`pms_duplicate`, with `existing`).",
14047
14858
  "content": {
14048
14859
  "application/json": {
14049
14860
  "schema": {
14050
14861
  "$ref": "#/components/schemas/Error"
14862
+ },
14863
+ "examples": {
14864
+ "pmsWritesOff": {
14865
+ "summary": "The connection is set not to change bookings through the API",
14866
+ "value": {
14867
+ "error": {
14868
+ "code": "pms_writes_off",
14869
+ "message": "Bookings for this property are managed in its PMS, and this connection is set not to change them through the API. Nothing was sent to the PMS.",
14870
+ "fix": "Turn on `reservations.api` with `PATCH /v1/connect/{provider}/write-policy`, or make the change in the PMS.",
14871
+ "docs_url": "https://repull.dev/docs/connect/write-policy",
14872
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
14873
+ }
14874
+ }
14875
+ },
14876
+ "noConnection": {
14877
+ "summary": "The listing is managed in a PMS this workspace is no longer connected to",
14878
+ "value": {
14879
+ "error": {
14880
+ "code": "no_connection",
14881
+ "message": "Listing 4118 is managed in Hostaway, but there is no Hostaway connection.",
14882
+ "fix": "The listing is managed in Hostaway, but this workspace has no connection to it. Reconnect with `POST /v1/connect/hostaway`, then retry. Nothing was sent to the PMS.",
14883
+ "docs_url": "https://repull.dev/docs/errors/no_connection",
14884
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14885
+ "provider": "hostaway",
14886
+ "reconnect": {
14887
+ "method": "POST",
14888
+ "path": "/v1/connect/hostaway"
14889
+ }
14890
+ }
14891
+ }
14892
+ },
14893
+ "pmsUnavailable": {
14894
+ "summary": "The dates are taken in the PMS",
14895
+ "value": {
14896
+ "error": {
14897
+ "code": "pms_unavailable",
14898
+ "message": "Guesty cannot take it: Guesty: listing is not available for the requested dates",
14899
+ "fix": "Guesty says the dates (or the unit) are not available. Pick other dates, or check them first with `POST /v1/reservations/quote`. Nothing was booked.",
14900
+ "docs_url": "https://repull.dev/docs/errors/pms_unavailable",
14901
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14902
+ "provider": "guesty",
14903
+ "pms": {
14904
+ "applied": [],
14905
+ "errors": [
14906
+ {
14907
+ "section": "reservation",
14908
+ "code": "unavailable",
14909
+ "message": "Guesty: listing is not available for the requested dates"
14910
+ }
14911
+ ]
14912
+ }
14913
+ }
14914
+ }
14915
+ },
14916
+ "pmsDuplicate": {
14917
+ "summary": "The PMS already holds a booking with this Idempotency-Key",
14918
+ "value": {
14919
+ "error": {
14920
+ "code": "pms_duplicate",
14921
+ "message": "Guesty already holds this booking: Guesty: a reservation with this originId already exists",
14922
+ "fix": "This request was already made with the same idempotency key; look the booking up instead of creating it again.",
14923
+ "docs_url": "https://repull.dev/docs/errors/pms_duplicate",
14924
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14925
+ "provider": "guesty",
14926
+ "existing": {
14927
+ "externalId": "64f1c2a9e1b2",
14928
+ "confirmationCode": "GY-a1B2c3D4"
14929
+ }
14930
+ }
14931
+ }
14932
+ }
14051
14933
  }
14052
14934
  }
14053
14935
  }
14054
14936
  },
14055
14937
  "422": {
14056
- "$ref": "#/components/responses/UnprocessableEntity"
14938
+ "description": "Invalid field (`invalid_params`), a field this listing cannot take (`unsupported_field`), something the PMS's API cannot do (`pms_write_unsupported`), the PMS refused the content (`pms_rejected`), or the pipeline declined (`reservation_not_created`).",
14939
+ "content": {
14940
+ "application/json": {
14941
+ "schema": {
14942
+ "$ref": "#/components/schemas/Error"
14943
+ },
14944
+ "examples": {
14945
+ "invalidParams": {
14946
+ "summary": "A field failed validation",
14947
+ "value": {
14948
+ "error": {
14949
+ "code": "invalid_params",
14950
+ "message": "`checkOut` is invalid: `checkOut` (2026-10-01) must be after `checkIn` (2026-10-05) \u2014 a stay spans at least one night.",
14951
+ "fix": "Send the departure date as `YYYY-MM-DD`, after `checkIn`, e.g. `\"checkOut\": \"2026-10-05\"`.",
14952
+ "docs_url": "https://repull.dev/docs/errors/invalid_params",
14953
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14954
+ "field": "checkOut",
14955
+ "value_received": "2026-10-01"
14956
+ }
14957
+ }
14958
+ },
14959
+ "unsupportedFieldPms": {
14960
+ "summary": "A direct-booking-only field on a PMS listing",
14961
+ "value": {
14962
+ "error": {
14963
+ "code": "unsupported_field",
14964
+ "message": "`checkInTime` cannot be set on a booking made in Hostaway: arrival and departure times come from the property's settings in the PMS.",
14965
+ "fix": "`GET /v1/listings/{id}` \u2192 `capabilities.reservations` shows what the listing supports. `totalPrice`, `notes`, `unitId` and `sendConfirmationEmail` are honoured only on listings managed in a PMS; `checkInTime`, `checkOutTime`, `currency` and `guestId` only on listings that are not.",
14966
+ "docs_url": "https://repull.dev/docs/errors/unsupported_field",
14967
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14968
+ "provider": "hostaway",
14969
+ "fields": [
14970
+ "checkInTime"
14971
+ ]
14972
+ }
14973
+ }
14974
+ },
14975
+ "unsupportedFieldDirect": {
14976
+ "summary": "A PMS-only field on a direct-booking listing",
14977
+ "value": {
14978
+ "error": {
14979
+ "code": "unsupported_field",
14980
+ "message": "`totalPrice` is only honoured for listings managed in a PMS. This listing is not.",
14981
+ "fix": "`GET /v1/listings/{id}` \u2192 `capabilities.reservations` shows what the listing supports. `totalPrice`, `notes`, `unitId` and `sendConfirmationEmail` are honoured only on listings managed in a PMS; `checkInTime`, `checkOutTime`, `currency` and `guestId` only on listings that are not.",
14982
+ "docs_url": "https://repull.dev/docs/errors/unsupported_field",
14983
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14984
+ "fields": [
14985
+ "totalPrice"
14986
+ ]
14987
+ }
14988
+ }
14989
+ },
14990
+ "pmsWriteUnsupported": {
14991
+ "summary": "Owner stays on a PMS listing",
14992
+ "value": {
14993
+ "error": {
14994
+ "code": "pms_write_unsupported",
14995
+ "message": "Bookings for this listing are managed in Hostaway. Block owner stays in Hostaway; the block reaches Repull with the next sync.",
14996
+ "fix": "Hostaway's API cannot create this booking. Do it in Hostaway; the change reaches Repull with the next sync.",
14997
+ "docs_url": "https://repull.dev/docs/errors/pms_write_unsupported",
14998
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
14999
+ "provider": "hostaway"
15000
+ }
15001
+ }
15002
+ },
15003
+ "pmsRejected": {
15004
+ "summary": "The PMS refused the content",
15005
+ "value": {
15006
+ "error": {
15007
+ "code": "pms_rejected",
15008
+ "message": "Hostaway refused to create the reservation: Hostaway: guestEmail is invalid",
15009
+ "fix": "Hostaway refused it; its reason is in `message`. Nothing was changed. Correct the request and retry with a NEW `Idempotency-Key`.",
15010
+ "docs_url": "https://repull.dev/docs/errors/pms_rejected",
15011
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15012
+ "provider": "hostaway",
15013
+ "pms": {
15014
+ "applied": [],
15015
+ "errors": [
15016
+ {
15017
+ "section": "reservation",
15018
+ "code": "rejected",
15019
+ "message": "Hostaway: guestEmail is invalid"
15020
+ }
15021
+ ]
15022
+ }
15023
+ }
15024
+ }
15025
+ }
15026
+ }
15027
+ }
15028
+ }
14057
15029
  },
14058
15030
  "500": {
14059
15031
  "$ref": "#/components/responses/InternalError"
15032
+ },
15033
+ "502": {
15034
+ "description": "The PMS could not be reached (`pms_error` \u2014 nothing confirmed; retry with the same key), or the booking was created in the PMS but not yet recorded in Repull (`reservation_created_in_pms_only` \u2014 do NOT retry; replayed for the same key).",
15035
+ "content": {
15036
+ "application/json": {
15037
+ "schema": {
15038
+ "$ref": "#/components/schemas/Error"
15039
+ },
15040
+ "examples": {
15041
+ "pmsError": {
15042
+ "summary": "The PMS could not be reached \u2014 retry with the same key",
15043
+ "value": {
15044
+ "error": {
15045
+ "code": "pms_error",
15046
+ "message": "Smoobu could not be reached to create the reservation: timeout of 30000ms exceeded",
15047
+ "fix": "Nothing was changed. Retry with the same idempotency key.",
15048
+ "docs_url": "https://repull.dev/docs/errors/pms_error",
15049
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15050
+ "provider": "smoobu"
15051
+ }
15052
+ }
15053
+ },
15054
+ "createdInPmsOnly": {
15055
+ "summary": "Created in the PMS, not yet recorded in Repull \u2014 do NOT retry",
15056
+ "value": {
15057
+ "error": {
15058
+ "code": "reservation_created_in_pms_only",
15059
+ "message": "Created in Lodgify (LG-88231) but its record could not be read here. It will arrive with the next sync.",
15060
+ "fix": "Do NOT retry. The booking exists in Lodgify (see `existing`) but could not be recorded in Repull yet; it arrives with the next sync and `reservation.created` fires then. Retrying with the same `Idempotency-Key` replays this answer.",
15061
+ "docs_url": "https://repull.dev/docs/errors/reservation_created_in_pms_only",
15062
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15063
+ "provider": "lodgify",
15064
+ "existing": {
15065
+ "externalId": "88231",
15066
+ "confirmationCode": "LG-88231"
15067
+ },
15068
+ "pms": {
15069
+ "applied": [
15070
+ "reservation"
15071
+ ],
15072
+ "errors": []
15073
+ },
15074
+ "retryable": false
15075
+ }
15076
+ }
15077
+ }
15078
+ }
15079
+ }
15080
+ }
14060
15081
  }
14061
15082
  }
14062
15083
  }
@@ -14114,7 +15135,7 @@
14114
15135
  "patch": {
14115
15136
  "operationId": "update_reservation",
14116
15137
  "summary": "Update a reservation",
14117
- "description": "Changes the dates, the occupancy, or the unit. Drives the same command path the dashboard does, so the side effects come with it: the change audit is appended, bound task due dates re-sync, the old calendar dates unblock and the new ones block, the conversation's cached listing is invalidated, and `reservation.updated` fires \u2014 which is what revokes and re-issues the door code.\n\nSupply at least one field; an empty body returns 422 rather than a 200 that changed nothing.\n\n**Moving and re-dating in one call is one operation.** Send `listingId` together with `checkIn`/`checkOut` and it is applied as a single move, so the access code is re-issued once rather than twice.\n\n### Fields this endpoint deliberately does NOT accept\n\nEach is rejected by name with the reason, never accepted and ignored:\n\n| Field | Why |\n|---|---|\n| `guest` / `guestDetails` | Guest name, email and phone live on the guest record. The underlying command has no branch for them, so accepting them would return a success that changed nothing. |\n| `pricing` / `totalPrice` / `currency` | Repricing writes the price breakdown, the pricing row and a pricing-history entry. It belongs to its own endpoint. |\n| `status` | Not a field. Cancelling, confirming and checking out are separate operations with materially different side effects \u2014 cancellation issues a credit refund and revokes access codes. |\n| `platform` | Immutable: it records where the booking actually originated. |\n| `notes` | `internal_notes` is an append-only audit trail the system writes on every change. |\n\n**Availability is NOT checked.** A date change that overlaps another booking will be written. Call `GET /v1/availability/{propertyId}` first if that matters.\n\nReturns `403 listing_inactive` when the reservation is on an inactive listing, or when a `listingId` move targets one; nothing is changed.",
15138
+ "description": "Changes the dates, the occupancy, or the unit.\n\n**A stay managed in a connected PMS is changed in the PMS first**, then Repull's copy is refreshed from the PMS's record \u2014 changing only Repull's copy would be reverted by the next sync. On such a stay, `checkIn`, `checkOut` and `guestCount` are changed; moving it to another listing and changing its check-in/check-out times are done in the PMS (`422 pms_write_unsupported`), as is anything the PMS's API cannot change (for example dates on Smoobu). A channel booking that came in through the PMS (Airbnb, Booking.com, \u2026) is changed on the channel (`409 reservation_owned_by_channel`). The PMS checks availability: taken dates answer `409 pms_unavailable`. The PMS's outcome comes back as `pms`. `GET /v1/listings/{id}` \u2192 `capabilities.reservations.modify` says whether a listing's PMS supports changes.\n\n**Any other stay** drives the same command path the dashboard does, so the side effects come with it: the change audit is appended, bound task due dates re-sync, the old calendar dates unblock and the new ones block, the conversation's cached listing is invalidated, and `reservation.updated` fires \u2014 which is what revokes and re-issues the door code.\n\nSupply at least one field; an empty body returns 422 rather than a 200 that changed nothing.\n\n**Moving and re-dating in one call is one operation.** Send `listingId` together with `checkIn`/`checkOut` and it is applied as a single move, so the access code is re-issued once rather than twice.\n\n### Fields this endpoint deliberately does NOT accept\n\nEach is rejected by name with the reason, never accepted and ignored:\n\n| Field | Why |\n|---|---|\n| `guest` / `guestDetails` | Guest name, email and phone live on the guest record. The underlying command has no branch for them, so accepting them would return a success that changed nothing. |\n| `pricing` / `totalPrice` / `currency` | Repricing writes the price breakdown, the pricing row and a pricing-history entry. It belongs to its own endpoint. |\n| `status` | Not a field. Cancelling, confirming and checking out are separate operations with materially different side effects \u2014 cancellation issues a credit refund and revokes access codes. |\n| `platform` | Immutable: it records where the booking actually originated. |\n| `notes` | `internal_notes` is an append-only audit trail the system writes on every change. |\n\n**Availability is NOT checked for stays outside a PMS.** A date change that overlaps another booking will be written. Call `GET /v1/availability/{propertyId}` first if that matters.\n\n`X-Account-Id` restricts the reservation (and a listing it is moved to) to one connected account. Returns `403 listing_inactive` when the reservation is on an inactive listing, or when a `listingId` move targets one; nothing is changed.",
14118
15139
  "tags": [
14119
15140
  "Reservations"
14120
15141
  ],
@@ -14130,6 +15151,16 @@
14130
15151
  },
14131
15152
  {
14132
15153
  "$ref": "#/components/parameters/IdempotencyKey"
15154
+ },
15155
+ {
15156
+ "name": "X-Account-Id",
15157
+ "in": "header",
15158
+ "required": false,
15159
+ "schema": {
15160
+ "type": "string",
15161
+ "example": "126"
15162
+ },
15163
+ "description": "Restrict the request to one connected account (a Repull connection id, `GET /v1/connect` \u2192 `id`). A listing or reservation outside that account answers `404 not_found`. Omit it to act workspace-wide."
14133
15164
  }
14134
15165
  ],
14135
15166
  "requestBody": {
@@ -14157,7 +15188,33 @@
14157
15188
  "$ref": "#/components/responses/Unauthorized"
14158
15189
  },
14159
15190
  "403": {
14160
- "$ref": "#/components/responses/ListingInactive"
15191
+ "description": "The reservation (or the listing it is moved to) is inactive (`listing_inactive`), or the PMS connection lacks write access to bookings (`connection_reauth_required`).",
15192
+ "content": {
15193
+ "application/json": {
15194
+ "schema": {
15195
+ "$ref": "#/components/schemas/Error"
15196
+ },
15197
+ "examples": {
15198
+ "reauthRequired": {
15199
+ "summary": "The grant lacks write access to bookings",
15200
+ "value": {
15201
+ "error": {
15202
+ "code": "connection_reauth_required",
15203
+ "message": "Beds24 needs to be reconnected before Repull can create reservations there: Beds24: token lacks scope write:bookings",
15204
+ "fix": "Reconnect Beds24 with an invite code that grants `write:bookings` (plus `bookings-personal` and `bookings-financial`), then retry.",
15205
+ "docs_url": "https://repull.dev/docs/errors/connection_reauth_required",
15206
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15207
+ "provider": "beds24",
15208
+ "reconnect": {
15209
+ "method": "POST",
15210
+ "path": "/v1/connect/beds24"
15211
+ }
15212
+ }
15213
+ }
15214
+ }
15215
+ }
15216
+ }
15217
+ }
14161
15218
  },
14162
15219
  "404": {
14163
15220
  "description": "The reservation, or the destination property when moving, does not exist in this workspace.",
@@ -14169,11 +15226,186 @@
14169
15226
  }
14170
15227
  }
14171
15228
  },
15229
+ "409": {
15230
+ "description": "The PMS will not change it: writes off for the API (`pms_writes_off`), no connection (`no_connection`), dates taken (`pms_unavailable`), a channel booking (`reservation_owned_by_channel`), or part of a group booking (`pms_group_booking`).",
15231
+ "content": {
15232
+ "application/json": {
15233
+ "schema": {
15234
+ "$ref": "#/components/schemas/Error"
15235
+ },
15236
+ "examples": {
15237
+ "pmsWritesOff": {
15238
+ "summary": "The connection is set not to change bookings through the API",
15239
+ "value": {
15240
+ "error": {
15241
+ "code": "pms_writes_off",
15242
+ "message": "Bookings for this property are managed in its PMS, and this connection is set not to change them through the API. Nothing was sent to the PMS.",
15243
+ "fix": "Turn on `reservations.api` with `PATCH /v1/connect/{provider}/write-policy`, or make the change in the PMS.",
15244
+ "docs_url": "https://repull.dev/docs/connect/write-policy",
15245
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
15246
+ }
15247
+ }
15248
+ },
15249
+ "noConnection": {
15250
+ "summary": "The listing is managed in a PMS this workspace is no longer connected to",
15251
+ "value": {
15252
+ "error": {
15253
+ "code": "no_connection",
15254
+ "message": "Listing 4118 is managed in Hostaway, but there is no Hostaway connection.",
15255
+ "fix": "The listing is managed in Hostaway, but this workspace has no connection to it. Reconnect with `POST /v1/connect/hostaway`, then retry. Nothing was sent to the PMS.",
15256
+ "docs_url": "https://repull.dev/docs/errors/no_connection",
15257
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15258
+ "provider": "hostaway",
15259
+ "reconnect": {
15260
+ "method": "POST",
15261
+ "path": "/v1/connect/hostaway"
15262
+ }
15263
+ }
15264
+ }
15265
+ },
15266
+ "pmsUnavailable": {
15267
+ "summary": "The dates are taken in the PMS",
15268
+ "value": {
15269
+ "error": {
15270
+ "code": "pms_unavailable",
15271
+ "message": "Guesty cannot take it: Guesty: listing is not available for the requested dates",
15272
+ "fix": "Guesty says the dates (or the unit) are not available. Pick other dates, or check them first with `POST /v1/reservations/quote`. Nothing was booked.",
15273
+ "docs_url": "https://repull.dev/docs/errors/pms_unavailable",
15274
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15275
+ "provider": "guesty",
15276
+ "pms": {
15277
+ "applied": [],
15278
+ "errors": [
15279
+ {
15280
+ "section": "reservation",
15281
+ "code": "unavailable",
15282
+ "message": "Guesty: listing is not available for the requested dates"
15283
+ }
15284
+ ]
15285
+ }
15286
+ }
15287
+ }
15288
+ },
15289
+ "ownedByChannel": {
15290
+ "summary": "A channel booking (incl. one relayed through a PMS)",
15291
+ "value": {
15292
+ "error": {
15293
+ "code": "reservation_owned_by_channel",
15294
+ "message": "This reservation came from airbnb through Hostaway. Change it on airbnb; the change reaches this workspace with the next sync.",
15295
+ "fix": "This booking belongs to a channel (Airbnb, Booking.com, Vrbo, \u2026). Change it on the channel; the change reaches Repull with the next sync and the matching webhook fires.",
15296
+ "docs_url": "https://repull.dev/docs/errors/reservation_owned_by_channel",
15297
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15298
+ "provider": "hostaway",
15299
+ "owner": "airbnb"
15300
+ }
15301
+ }
15302
+ },
15303
+ "groupBooking": {
15304
+ "summary": "Part of a group booking in the PMS",
15305
+ "value": {
15306
+ "error": {
15307
+ "code": "pms_group_booking",
15308
+ "message": "This stay is one room of a Cloudbeds group booking; change it in Cloudbeds.",
15309
+ "fix": "This stay is part of a group booking in Cloudbeds. Change or cancel group bookings in Cloudbeds; the change reaches Repull with the next sync.",
15310
+ "docs_url": "https://repull.dev/docs/errors/pms_group_booking",
15311
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15312
+ "provider": "cloudbeds"
15313
+ }
15314
+ }
15315
+ }
15316
+ }
15317
+ }
15318
+ }
15319
+ },
14172
15320
  "422": {
14173
- "$ref": "#/components/responses/UnprocessableEntity"
15321
+ "description": "Invalid or refused field (`invalid_params`, `unsupported_field`), something the PMS's API cannot change (`pms_write_unsupported`), the PMS refused the change (`pms_rejected`), or the command declined (`reservation_not_modified`).",
15322
+ "content": {
15323
+ "application/json": {
15324
+ "schema": {
15325
+ "$ref": "#/components/schemas/Error"
15326
+ },
15327
+ "examples": {
15328
+ "invalidParams": {
15329
+ "summary": "A field failed validation",
15330
+ "value": {
15331
+ "error": {
15332
+ "code": "invalid_params",
15333
+ "message": "`checkOut` is invalid: `checkOut` (2026-10-01) must be after `checkIn` (2026-10-05) \u2014 a stay spans at least one night.",
15334
+ "fix": "Send the departure date as `YYYY-MM-DD`, after `checkIn`, e.g. `\"checkOut\": \"2026-10-05\"`.",
15335
+ "docs_url": "https://repull.dev/docs/errors/invalid_params",
15336
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15337
+ "field": "checkOut",
15338
+ "value_received": "2026-10-01"
15339
+ }
15340
+ }
15341
+ },
15342
+ "pmsWriteUnsupported": {
15343
+ "summary": "Dates on Smoobu",
15344
+ "value": {
15345
+ "error": {
15346
+ "code": "pms_write_unsupported",
15347
+ "message": "Smoobu cannot change the dates of a booking through its API. Cancel and rebook, or change them in Smoobu.",
15348
+ "fix": "Smoobu's API cannot change this booking. Do it in Smoobu; the change reaches Repull with the next sync.",
15349
+ "docs_url": "https://repull.dev/docs/errors/pms_write_unsupported",
15350
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15351
+ "provider": "smoobu"
15352
+ }
15353
+ }
15354
+ },
15355
+ "pmsRejected": {
15356
+ "summary": "The PMS refused the content",
15357
+ "value": {
15358
+ "error": {
15359
+ "code": "pms_rejected",
15360
+ "message": "Hostaway refused to create the reservation: Hostaway: guestEmail is invalid",
15361
+ "fix": "Hostaway refused it; its reason is in `message`. Nothing was changed. Correct the request and retry with a NEW `Idempotency-Key`.",
15362
+ "docs_url": "https://repull.dev/docs/errors/pms_rejected",
15363
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15364
+ "provider": "hostaway",
15365
+ "pms": {
15366
+ "applied": [],
15367
+ "errors": [
15368
+ {
15369
+ "section": "reservation",
15370
+ "code": "rejected",
15371
+ "message": "Hostaway: guestEmail is invalid"
15372
+ }
15373
+ ]
15374
+ }
15375
+ }
15376
+ }
15377
+ }
15378
+ }
15379
+ }
15380
+ }
14174
15381
  },
14175
15382
  "500": {
14176
15383
  "$ref": "#/components/responses/InternalError"
15384
+ },
15385
+ "502": {
15386
+ "description": "The PMS could not be reached (`pms_error`) \u2014 nothing was changed; retry.",
15387
+ "content": {
15388
+ "application/json": {
15389
+ "schema": {
15390
+ "$ref": "#/components/schemas/Error"
15391
+ },
15392
+ "examples": {
15393
+ "pmsError": {
15394
+ "summary": "The PMS could not be reached \u2014 retry with the same key",
15395
+ "value": {
15396
+ "error": {
15397
+ "code": "pms_error",
15398
+ "message": "Smoobu could not be reached to create the reservation: timeout of 30000ms exceeded",
15399
+ "fix": "Nothing was changed. Retry with the same idempotency key.",
15400
+ "docs_url": "https://repull.dev/docs/errors/pms_error",
15401
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
15402
+ "provider": "smoobu"
15403
+ }
15404
+ }
15405
+ }
15406
+ }
15407
+ }
15408
+ }
14177
15409
  }
14178
15410
  }
14179
15411
  }
@@ -14427,6 +15659,9 @@
14427
15659
  "Conversations"
14428
15660
  ],
14429
15661
  "parameters": [
15662
+ {
15663
+ "$ref": "#/components/parameters/Account"
15664
+ },
14430
15665
  {
14431
15666
  "$ref": "#/components/parameters/XSchemaHeader"
14432
15667
  },
@@ -15065,6 +16300,9 @@
15065
16300
  "Reviews"
15066
16301
  ],
15067
16302
  "parameters": [
16303
+ {
16304
+ "$ref": "#/components/parameters/Account"
16305
+ },
15068
16306
  {
15069
16307
  "$ref": "#/components/parameters/XSchemaHeader"
15070
16308
  },
@@ -15287,7 +16525,7 @@
15287
16525
  "get": {
15288
16526
  "operationId": "list_connections",
15289
16527
  "summary": "List PMS/OTA connections",
15290
- "description": "Returns all active connections to PMS and OTA platforms.",
16528
+ "description": "Returns every PMS and OTA connection in the workspace, each with its `status`.\n\n**Spot connections that need attention.** A connection whose `status` is not `active` may need the host to do something before it works \u2014 most commonly a Booking.com Extranet connection where the invited user was granted only partial access (`status: \"needs_permissions\"`). A Smoobu connection still on a legacy single API key carries `action.reason: \"reauth_required\"` while its `status` is `active`: Smoobu stops accepting those keys on October 31, 2026, and `fixUrl` opens the form for a new API key + secret (the connection id stays the same). These connections carry two extra fields:\n\n- `action` \u2014 `{ required: true, reason, message }`. `reason` is a stable machine code (e.g. `needs_permissions`); `message` is a host-facing one-liner describing what to do.\n- `fixUrl` \u2014 a durable link that reopens the hosted Connect flow **bound to that account, on the fix screen** (e.g. \"grant full access\" + a Re-check button). It is safe to store and show in your own dashboard.\n\n**Self-serve repair:** when `action.required` is true, surface a \"Fix\" button that opens `fixUrl` in a new tab (or embed it). The host resolves the issue (e.g. grants the user full access in Booking.com) and clicks Re-check; the import finishes on its own and the connection flips back to `active` \u2014 no re-invite, no support ticket. Poll this endpoint (or read it after the host returns) to confirm `action` has cleared.",
15291
16529
  "tags": [
15292
16530
  "Connect"
15293
16531
  ],
@@ -15329,7 +16567,14 @@
15329
16567
  "state": {
15330
16568
  "type": "string",
15331
16569
  "nullable": true,
15332
- "description": "Opaque pass-through correlation token. Echoed back in the response."
16570
+ "description": "Your own correlation token, e.g. your user id (at most 500 characters). Echoed in this response, on the redirect back (`&state=`), in the popup message, and in the `connect.session.completed` webhook."
16571
+ },
16572
+ "reservationHistoryMonths": {
16573
+ "type": "integer",
16574
+ "minimum": 1,
16575
+ "maximum": 60,
16576
+ "example": 24,
16577
+ "description": "Airbnb \u2014 how many months of past reservations the first import pulls (1\u201360). Omit it for the default window. Upcoming stays are always imported. A wider window takes longer to import, because every extra month is more stays to fetch."
15333
16578
  },
15334
16579
  "accessType": {
15335
16580
  "type": "string",
@@ -15552,7 +16797,7 @@
15552
16797
  "post": {
15553
16798
  "operationId": "create_connection",
15554
16799
  "summary": "Connect to PMS/OTA provider",
15555
- "description": "Establish a connection to a PMS or OTA platform. Credentials vary by provider \u2014 see docs for each provider.\n\nAirbnb-specific: pass `redirectUrl` (where to send the user after consent) and optionally `accessType`. Three tiers: `read_only` grants read-only scopes; `messaging` grants read scopes plus message read/send but NOT property management, so it can coexist with another app (e.g. an existing PMS) that already holds property management on the same Airbnb account; `full_access` \u2014 the default \u2014 grants full host scopes including the exclusive property management (only one app per Airbnb account can hold it). The response returns a hosted `url` to redirect the user to.\n\nBooking.com: pass `redirectUrl` (no `accessType`). The response returns a hosted `url` \u2014 send the user there to designate FantasticStay in their Booking.com Extranet and paste their Hotel ID. Same response shape as Airbnb (`url`, `sessionId`, `expiresAt`).\n\nPMS providers (api-key based) pass `apiKey` instead; Plumguide passes `clientId`/`clientSecret`.",
16800
+ "description": "Establish a connection to a PMS or OTA platform. Credentials vary by provider \u2014 see docs for each provider.\n\nAirbnb-specific: pass `redirectUrl` (where to send the user after consent) and optionally `accessType`. Three tiers: `read_only` grants read-only scopes; `messaging` grants read scopes plus message read/send but NOT property management, so it can coexist with another app (e.g. an existing PMS) that already holds property management on the same Airbnb account; `full_access` \u2014 the default \u2014 grants full host scopes including the exclusive property management (only one app per Airbnb account can hold it). The response returns a hosted `url` to redirect the user to.\n\nBooking.com: pass `redirectUrl` (no `accessType`). The response returns a hosted `url` \u2014 send the user there to designate FantasticStay in their Booking.com Extranet and paste their Hotel ID. Same response shape as Airbnb (`url`, `sessionId`, `expiresAt`).\n\nPlumguide passes `clientId`/`clientSecret`. PMS providers are not connected by this call: send their credentials to `POST /v1/connect/{provider}/credentials`, or start a hosted session with `POST /v1/connect`.",
15556
16801
  "tags": [
15557
16802
  "Connect"
15558
16803
  ],
@@ -15583,9 +16828,16 @@
15583
16828
  "default": "full_access",
15584
16829
  "description": "Airbnb only \u2014 selects the OAuth scope set. 'read_only' grants read-only scopes; 'messaging' grants read scopes plus message read/send but NOT property management, so it can coexist with another app (e.g. an existing PMS) that already holds property management on the same Airbnb account; 'full_access' (default) grants full host scopes including the exclusive property management (only one app per Airbnb account can hold it). The hosted consent screen normally lets the host pick a tier; passing `accessType` explicitly fixes the tier and hides that choice, so the host can only continue with the tier you requested. Omit it to let the host choose."
15585
16830
  },
15586
- "apiKey": {
16831
+ "reservationHistoryMonths": {
16832
+ "type": "integer",
16833
+ "minimum": 1,
16834
+ "maximum": 60,
16835
+ "example": 24,
16836
+ "description": "Airbnb \u2014 how many months of past reservations the first import pulls (1\u201360). Omit it for the default window. Upcoming stays are always imported. A wider window takes longer to import, because every extra month is more stays to fetch."
16837
+ },
16838
+ "state": {
15587
16839
  "type": "string",
15588
- "description": "PMS providers \u2014 API key."
16840
+ "description": "Airbnb + Booking.com \u2014 your own correlation token, e.g. your user id (at most 500 characters). Echoed on the redirect back (`&state=`) and in the `connect.session.completed` webhook."
15589
16841
  },
15590
16842
  "clientId": {
15591
16843
  "type": "string",
@@ -16242,7 +17494,7 @@
16242
17494
  "post": {
16243
17495
  "operationId": "submitSmoobuCredentials",
16244
17496
  "summary": "Submit Smoobu credentials for a Connect session",
16245
- "description": "Completes a credentials-pattern connection for Smoobu. API key from Smoobu \u2192 Settings \u2192 For developers.\n\nThe credentials are validated against Smoobu before anything is persisted, so an invalid pair returns `invalid_credentials` rather than creating a dead connection. On success the `pms_connections` row is written and the Connect session moves to its terminal state.\n\nNo API key required when called with a `sessionId` \u2014 the session is the capability token.",
17497
+ "description": "Completes a credentials-pattern connection for Smoobu with an HMAC API key + API secret, created in Smoobu \u2192 Settings \u2192 Advanced \u2192 API Keys (Create API Key, then Generate Secret \u2014 the secret is shown only once). Smoobu retires single legacy API keys on October 31, 2026, so `apiSecret` is required; a request with only `apiKey` returns `invalid_params`.\n\nReconnecting replaces the stored credentials on the workspace's existing Smoobu connection \u2014 the `pmsConnectionId` stays the same.\n\nThe credentials are validated against Smoobu before anything is persisted, so an invalid pair returns `invalid_credentials` rather than creating a dead connection. On success the `pms_connections` row is written and the Connect session moves to its terminal state.\n\nNo API key required when called with a `sessionId` \u2014 the session is the capability token.",
16246
17498
  "tags": [
16247
17499
  "Connect"
16248
17500
  ],
@@ -16260,7 +17512,21 @@
16260
17512
  "credentials": {
16261
17513
  "type": "object",
16262
17514
  "additionalProperties": true,
16263
- "description": "API key from Smoobu \u2192 Settings \u2192 For developers."
17515
+ "description": "HMAC API key + secret from Smoobu \u2192 Settings \u2192 Advanced \u2192 API Keys.",
17516
+ "properties": {
17517
+ "apiKey": {
17518
+ "type": "string",
17519
+ "description": "Smoobu API key."
17520
+ },
17521
+ "apiSecret": {
17522
+ "type": "string",
17523
+ "description": "Smoobu API secret (shown once when generated)."
17524
+ }
17525
+ },
17526
+ "required": [
17527
+ "apiKey",
17528
+ "apiSecret"
17529
+ ]
16264
17530
  }
16265
17531
  },
16266
17532
  "required": [
@@ -17598,11 +18864,26 @@
17598
18864
  "get": {
17599
18865
  "operationId": "list_airbnb_listings",
17600
18866
  "summary": "List Airbnb listings",
17601
- "description": "List every Airbnb listing this workspace has access to via the connected Airbnb account. **Pure DB read \u2014 never calls Airbnb upstream.** The connect flow is what populates the local cache; the API serves what's already there. Customers with a disconnected host still see their last-synced data, with the top-level `dataFreshness` envelope flagging the staleness and pointing at the reconnect URL.\n\nPass `?include=amenities` to enrich each connection with its locally-cached amenity set. Returns `null` per connection when the cache is empty.\n\nPass `?include=thumbnail` to add `thumbnailUrl` to each listing \u2014 one extra column on the query that already runs, so a selection screen renders from a single request instead of one call per listing. `null` when the listing has no thumbnail stored. Combine comma-separated, e.g. `?include=amenities,thumbnail`.\n\n**Can this listing be written to?** Every connection carries `syncCategory` \u2014 Airbnb's own per-listing API sync decision (`sync_all`, `sync_rates_and_availability`, or `none`) \u2014 and `writable`, which is `false` exactly when that category is `none`. Airbnb authorises sync one listing at a time, so a connected account can still hold listings Airbnb refuses every write to; a write to one of those returns `403 listing_not_api_connected` before anything is sent, and reconnecting the account does not change it (the host must switch the listing on in Airbnb). Check `writable` here before a portfolio-wide push instead of discovering it one 403 at a time.\n\nInactive listings are left out; they keep syncing and reappear once activated. Use `GET /v1/listings?status=inactive` to find them.\n\n**Several Airbnb accounts?** A workspace can connect more than one. By default this returns every connected account's rows; pass `?account_id=<airbnb host id>` to scope to one. Every row carries `accountId` + `accountName` either way, and `dataFreshness.accounts[]` reports each account's freshness separately, so one disconnected host no longer marks the whole response stale.",
18867
+ "description": "List every Airbnb listing this workspace has access to via the connected Airbnb account. **Pure DB read \u2014 never calls Airbnb upstream.** The connect flow is what populates the local cache; the API serves what's already there. Customers with a disconnected host still see their last-synced data, with the top-level `dataFreshness` envelope flagging the staleness and pointing at the reconnect URL.\n\nPass `?include=amenities` to enrich each connection with its locally-cached amenity set. Returns `null` per connection when the cache is empty.\n\nPass `?include=thumbnail` to add `thumbnailUrl` to each listing \u2014 one extra column on the query that already runs, so a selection screen renders from a single request instead of one call per listing. `null` when the listing has no thumbnail stored. Combine comma-separated, e.g. `?include=amenities,thumbnail`.\n\n**Can this listing be written to?** Every connection carries `syncCategory` \u2014 Airbnb's own per-listing API sync decision (`sync_all`, `sync_rates_and_availability`, or `none`) \u2014 and `writable`, which is `false` exactly when that category is `none`. Airbnb authorises sync one listing at a time, so a connected account can still hold listings Airbnb refuses every write to; a write to one of those returns `403 listing_not_api_connected` before anything is sent, and reconnecting the account does not change it (the host must switch the listing on in Airbnb). Check `writable` here before a portfolio-wide push instead of discovering it one 403 at a time.\n\nInactive listings are left out unless `?status=inactive|all` asks for them; they then come back with identity fields only \u2014 `listingId`, `name`, `city`, `status`, `inactiveReason` (`plan_limit`, `unlisted_on_airbnb` or `deactivated`) and each connection's ids and account \u2014 so you can show the user what to activate. They keep syncing and are complete again once activated.\n\n**Several Airbnb accounts?** A workspace can connect more than one. By default this returns every connected account's rows; pass `?account_id=<airbnb host id>` to scope to one. Every row carries `accountId` + `accountName` either way, and `dataFreshness.accounts[]` reports each account's freshness separately, so one disconnected host no longer marks the whole response stale.",
17602
18868
  "tags": [
17603
18869
  "Airbnb"
17604
18870
  ],
17605
18871
  "parameters": [
18872
+ {
18873
+ "name": "status",
18874
+ "in": "query",
18875
+ "required": false,
18876
+ "schema": {
18877
+ "type": "string",
18878
+ "enum": [
18879
+ "active",
18880
+ "inactive",
18881
+ "all"
18882
+ ],
18883
+ "default": "active"
18884
+ },
18885
+ "description": "`active` (default) leaves inactive listings out. `inactive` returns only them and `all` returns both. An inactive listing comes back with identity fields only (ids, `name`, `city`, `status`, `inactiveReason`, its account), which is enough to show what can be activated. Every row carries `status`."
18886
+ },
17606
18887
  {
17607
18888
  "$ref": "#/components/parameters/AirbnbAccountId"
17608
18889
  },
@@ -20055,9 +21336,230 @@
20055
21336
  }
20056
21337
  },
20057
21338
  "put": {
20058
- "operationId": "update_airbnb_listing_amenities",
20059
- "summary": "Update Airbnb amenities",
20060
- "description": "Set amenities on an Airbnb listing. **Write-side** \u2014 calls Airbnb upstream.\n\n**Partial by design**: only the amenities you name change, so turning one off is a one-line body and nothing else on the listing moves. Ids are the `id` values `GET /amenities` returns (e.g. `wireless_internet`, `ac`, `kitchen`); case is ignored. Airbnb refuses ids outside its vocabulary \u2014 that comes back as `422 airbnb_rejected` carrying Airbnb's own message.\n\n`accessibility_amenities` go to Airbnb's separate accessibility resource, which has **no read side at all** \u2014 Airbnb offers no endpoint to fetch them back, and the combined amenities GET 404s on production listings. What you can read back is our own copy: this endpoint updates it on success, and `GET /amenities` returns it under `accessibilityAmenities`. Airbnb may also hold an accessibility claim for review until photo evidence is attached; pass `photo_ids` to supply it.\n\nOur Airbnb copy is updated on success so a read straight after this write returns the new values. The platform-neutral copy behind `GET /v1/listings/{id}?include=amenities` uses a different amenity vocabulary and is refreshed by the next sync, except where an id happens to be identical in both.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
21339
+ "operationId": "update_airbnb_listing_amenities",
21340
+ "summary": "Update Airbnb amenities",
21341
+ "description": "Set amenities on an Airbnb listing. **Write-side** \u2014 calls Airbnb upstream.\n\n**Partial by design**: only the amenities you name change, so turning one off is a one-line body and nothing else on the listing moves. Ids are the `id` values `GET /amenities` returns (e.g. `wireless_internet`, `ac`, `kitchen`); case is ignored. Airbnb refuses ids outside its vocabulary \u2014 that comes back as `422 airbnb_rejected` carrying Airbnb's own message.\n\n`accessibility_amenities` go to Airbnb's separate accessibility resource, which has **no read side at all** \u2014 Airbnb offers no endpoint to fetch them back, and the combined amenities GET 404s on production listings. What you can read back is our own copy: this endpoint updates it on success, and `GET /amenities` returns it under `accessibilityAmenities`. Airbnb may also hold an accessibility claim for review until photo evidence is attached; pass `photo_ids` to supply it.\n\nOur Airbnb copy is updated on success so a read straight after this write returns the new values. The platform-neutral copy behind `GET /v1/listings/{id}?include=amenities` uses a different amenity vocabulary and is refreshed by the next sync, except where an id happens to be identical in both.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
21342
+ "tags": [
21343
+ "Airbnb"
21344
+ ],
21345
+ "parameters": [
21346
+ {
21347
+ "name": "id",
21348
+ "in": "path",
21349
+ "required": true,
21350
+ "schema": {
21351
+ "type": "string"
21352
+ },
21353
+ "description": "Repull listing id (numeric string)."
21354
+ }
21355
+ ],
21356
+ "requestBody": {
21357
+ "required": true,
21358
+ "content": {
21359
+ "application/json": {
21360
+ "schema": {
21361
+ "type": "object",
21362
+ "additionalProperties": false,
21363
+ "description": "At least one amenity across `amenities` and `accessibility_amenities`. A body that changes nothing is refused rather than reported as a successful write.",
21364
+ "properties": {
21365
+ "amenities": {
21366
+ "type": "array",
21367
+ "maxItems": 200,
21368
+ "items": {
21369
+ "type": "object",
21370
+ "required": [
21371
+ "id",
21372
+ "is_present"
21373
+ ],
21374
+ "additionalProperties": false,
21375
+ "properties": {
21376
+ "id": {
21377
+ "type": "string",
21378
+ "description": "Airbnb amenity id, e.g. `wireless_internet`. Case is ignored."
21379
+ },
21380
+ "is_present": {
21381
+ "type": "boolean",
21382
+ "description": "`true` claims the amenity, `false` removes it. Required \u2014 an amenity with no `is_present` would be a silent no-op."
21383
+ },
21384
+ "instruction": {
21385
+ "type": "string",
21386
+ "nullable": true,
21387
+ "maxLength": 500,
21388
+ "description": "Optional host note shown with the amenity."
21389
+ }
21390
+ }
21391
+ }
21392
+ },
21393
+ "accessibility_amenities": {
21394
+ "type": "array",
21395
+ "maxItems": 100,
21396
+ "items": {
21397
+ "type": "object",
21398
+ "required": [
21399
+ "id",
21400
+ "is_present"
21401
+ ],
21402
+ "additionalProperties": false,
21403
+ "properties": {
21404
+ "id": {
21405
+ "type": "string"
21406
+ },
21407
+ "is_present": {
21408
+ "type": "boolean"
21409
+ },
21410
+ "instruction": {
21411
+ "type": "string",
21412
+ "nullable": true,
21413
+ "maxLength": 500
21414
+ },
21415
+ "photo_ids": {
21416
+ "type": "array",
21417
+ "maxItems": 20,
21418
+ "items": {
21419
+ "type": "string"
21420
+ },
21421
+ "description": "Airbnb photo ids evidencing the accessibility claim (`photoAirbnbId` from `GET /photos`)."
21422
+ }
21423
+ }
21424
+ }
21425
+ }
21426
+ }
21427
+ }
21428
+ }
21429
+ }
21430
+ },
21431
+ "responses": {
21432
+ "200": {
21433
+ "description": "Amenities updated",
21434
+ "content": {
21435
+ "application/json": {
21436
+ "schema": {
21437
+ "type": "object",
21438
+ "required": [
21439
+ "data",
21440
+ "stored"
21441
+ ],
21442
+ "properties": {
21443
+ "data": {
21444
+ "type": "object",
21445
+ "properties": {
21446
+ "amenities": {
21447
+ "type": "integer",
21448
+ "description": "How many regular amenities were written."
21449
+ },
21450
+ "accessibilityAmenities": {
21451
+ "type": "integer",
21452
+ "description": "How many accessibility amenities were written."
21453
+ }
21454
+ }
21455
+ },
21456
+ "stored": {
21457
+ "type": "boolean",
21458
+ "description": "Whether our own copy was brought in line with the change."
21459
+ }
21460
+ }
21461
+ }
21462
+ }
21463
+ }
21464
+ },
21465
+ "401": {
21466
+ "$ref": "#/components/responses/Unauthorized"
21467
+ },
21468
+ "403": {
21469
+ "$ref": "#/components/responses/ListingInactive"
21470
+ },
21471
+ "404": {
21472
+ "$ref": "#/components/responses/NotFound"
21473
+ },
21474
+ "422": {
21475
+ "$ref": "#/components/responses/UnprocessableEntity"
21476
+ },
21477
+ "429": {
21478
+ "$ref": "#/components/responses/AirbnbRateLimited"
21479
+ },
21480
+ "500": {
21481
+ "$ref": "#/components/responses/InternalError"
21482
+ }
21483
+ }
21484
+ }
21485
+ },
21486
+ "/v1/channels/airbnb/listings/{id}/checkin-guide": {
21487
+ "get": {
21488
+ "operationId": "get_airbnb_checkin_guide",
21489
+ "summary": "Get Airbnb check-in guide",
21490
+ "description": "Return every published locale variant of an Airbnb listing's check-in guide. **Pure DB read** from `listings_airbnb_check_in_guides`. Pass `?locale=en` to filter to one locale (prefix match). Returns `404` when the listing has no Airbnb connection in this workspace.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
21491
+ "tags": [
21492
+ "Airbnb"
21493
+ ],
21494
+ "parameters": [
21495
+ {
21496
+ "name": "id",
21497
+ "in": "path",
21498
+ "required": true,
21499
+ "schema": {
21500
+ "type": "string"
21501
+ },
21502
+ "description": "Repull listing id (numeric string)."
21503
+ },
21504
+ {
21505
+ "name": "locale",
21506
+ "in": "query",
21507
+ "required": false,
21508
+ "schema": {
21509
+ "type": "string",
21510
+ "example": "en"
21511
+ },
21512
+ "description": "Filter to a single locale (prefix match, case-insensitive)."
21513
+ }
21514
+ ],
21515
+ "responses": {
21516
+ "200": {
21517
+ "description": "Check-in guides",
21518
+ "content": {
21519
+ "application/json": {
21520
+ "schema": {
21521
+ "type": "object",
21522
+ "required": [
21523
+ "data",
21524
+ "dataFreshness"
21525
+ ],
21526
+ "properties": {
21527
+ "data": {
21528
+ "type": "array",
21529
+ "items": {
21530
+ "type": "object",
21531
+ "additionalProperties": true
21532
+ }
21533
+ },
21534
+ "dataFreshness": {
21535
+ "$ref": "#/components/schemas/AirbnbDataFreshness"
21536
+ }
21537
+ }
21538
+ }
21539
+ }
21540
+ }
21541
+ },
21542
+ "401": {
21543
+ "$ref": "#/components/responses/Unauthorized"
21544
+ },
21545
+ "403": {
21546
+ "$ref": "#/components/responses/ListingInactive"
21547
+ },
21548
+ "404": {
21549
+ "$ref": "#/components/responses/NotFound"
21550
+ },
21551
+ "422": {
21552
+ "$ref": "#/components/responses/UnprocessableEntity"
21553
+ },
21554
+ "500": {
21555
+ "$ref": "#/components/responses/InternalError"
21556
+ }
21557
+ }
21558
+ },
21559
+ "put": {
21560
+ "operationId": "update_airbnb_checkin_guide",
21561
+ "summary": "Replace the steps of an Airbnb check-in guide",
21562
+ "description": "Write the check-in guide guests see before arrival: an ordered list of text steps. **Replaces** every existing step, so send the whole guide; `{\"steps\": []}` removes them all. The response is the guide re-read from Airbnb after the write.\n\nIf the listing has no guide yet, one is created in `locale` (default: the existing guide's, else `en`).\n\nSafe on failure: the new steps are created before the old ones are removed, and if a create fails the steps this call added are removed again, so the guide is never left emptier than it was.\n\nText steps only. Steps with photos need Airbnb's media upload and are not supported here yet. For the other arrival details use `PUT /v1/channels/airbnb/listings/{id}/details`: `check_in_option.instruction` (arrival instructions), `house_manual`, `directions`, `wifi_network`, `wifi_password`.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
20061
21563
  "tags": [
20062
21564
  "Airbnb"
20063
21565
  ],
@@ -20078,103 +21580,91 @@
20078
21580
  "application/json": {
20079
21581
  "schema": {
20080
21582
  "type": "object",
21583
+ "required": [
21584
+ "steps"
21585
+ ],
20081
21586
  "additionalProperties": false,
20082
- "description": "At least one amenity across `amenities` and `accessibility_amenities`. A body that changes nothing is refused rather than reported as a successful write.",
20083
21587
  "properties": {
20084
- "amenities": {
20085
- "type": "array",
20086
- "maxItems": 200,
20087
- "items": {
20088
- "type": "object",
20089
- "required": [
20090
- "id",
20091
- "is_present"
20092
- ],
20093
- "additionalProperties": false,
20094
- "properties": {
20095
- "id": {
20096
- "type": "string",
20097
- "description": "Airbnb amenity id, e.g. `wireless_internet`. Case is ignored."
20098
- },
20099
- "is_present": {
20100
- "type": "boolean",
20101
- "description": "`true` claims the amenity, `false` removes it. Required \u2014 an amenity with no `is_present` would be a silent no-op."
20102
- },
20103
- "instruction": {
20104
- "type": "string",
20105
- "nullable": true,
20106
- "maxLength": 500,
20107
- "description": "Optional host note shown with the amenity."
20108
- }
20109
- }
20110
- }
21588
+ "locale": {
21589
+ "type": "string",
21590
+ "example": "en",
21591
+ "description": "Language of the guide when one has to be created. Ignored when the listing already has a guide."
20111
21592
  },
20112
- "accessibility_amenities": {
21593
+ "steps": {
20113
21594
  "type": "array",
20114
- "maxItems": 100,
21595
+ "maxItems": 30,
21596
+ "description": "The guide's steps, in the order guests see them.",
20115
21597
  "items": {
20116
21598
  "type": "object",
20117
21599
  "required": [
20118
- "id",
20119
- "is_present"
21600
+ "notes"
20120
21601
  ],
20121
21602
  "additionalProperties": false,
20122
21603
  "properties": {
20123
- "id": {
20124
- "type": "string"
20125
- },
20126
- "is_present": {
20127
- "type": "boolean"
20128
- },
20129
- "instruction": {
21604
+ "notes": {
20130
21605
  "type": "string",
20131
- "nullable": true,
20132
- "maxLength": 500
20133
- },
20134
- "photo_ids": {
20135
- "type": "array",
20136
- "maxItems": 20,
20137
- "items": {
20138
- "type": "string"
20139
- },
20140
- "description": "Airbnb photo ids evidencing the accessibility claim (`photoAirbnbId` from `GET /photos`)."
21606
+ "minLength": 1,
21607
+ "maxLength": 5000
20141
21608
  }
20142
21609
  }
20143
21610
  }
20144
21611
  }
20145
21612
  }
21613
+ },
21614
+ "example": {
21615
+ "steps": [
21616
+ {
21617
+ "notes": "The lockbox is to the right of the front door. Your code arrives by message on the day of arrival."
21618
+ },
21619
+ {
21620
+ "notes": "Parking: use spot 12 in the garage under the building."
21621
+ }
21622
+ ]
20146
21623
  }
20147
21624
  }
20148
21625
  }
20149
21626
  },
20150
21627
  "responses": {
20151
21628
  "200": {
20152
- "description": "Amenities updated",
21629
+ "description": "The guide as Airbnb holds it after the write",
20153
21630
  "content": {
20154
21631
  "application/json": {
20155
21632
  "schema": {
20156
21633
  "type": "object",
20157
- "required": [
20158
- "data",
20159
- "stored"
20160
- ],
20161
21634
  "properties": {
20162
21635
  "data": {
20163
21636
  "type": "object",
20164
21637
  "properties": {
20165
- "amenities": {
20166
- "type": "integer",
20167
- "description": "How many regular amenities were written."
21638
+ "listingId": {
21639
+ "type": "integer"
20168
21640
  },
20169
- "accessibilityAmenities": {
20170
- "type": "integer",
20171
- "description": "How many accessibility amenities were written."
21641
+ "locale": {
21642
+ "type": "string"
21643
+ },
21644
+ "published": {
21645
+ "type": "boolean",
21646
+ "nullable": true
21647
+ },
21648
+ "steps": {
21649
+ "type": "array",
21650
+ "items": {
21651
+ "type": "object",
21652
+ "properties": {
21653
+ "id": {
21654
+ "type": "integer"
21655
+ },
21656
+ "notes": {
21657
+ "type": "string",
21658
+ "nullable": true
21659
+ },
21660
+ "mediaUrl": {
21661
+ "type": "string",
21662
+ "nullable": true
21663
+ }
21664
+ }
21665
+ }
20172
21666
  }
20173
21667
  }
20174
- },
20175
- "stored": {
20176
- "type": "boolean",
20177
- "description": "Whether our own copy was brought in line with the change."
20178
21668
  }
20179
21669
  }
20180
21670
  }
@@ -20184,132 +21674,6 @@
20184
21674
  "401": {
20185
21675
  "$ref": "#/components/responses/Unauthorized"
20186
21676
  },
20187
- "403": {
20188
- "$ref": "#/components/responses/ListingInactive"
20189
- },
20190
- "404": {
20191
- "$ref": "#/components/responses/NotFound"
20192
- },
20193
- "422": {
20194
- "$ref": "#/components/responses/UnprocessableEntity"
20195
- },
20196
- "429": {
20197
- "$ref": "#/components/responses/AirbnbRateLimited"
20198
- },
20199
- "500": {
20200
- "$ref": "#/components/responses/InternalError"
20201
- }
20202
- }
20203
- }
20204
- },
20205
- "/v1/channels/airbnb/listings/{id}/checkin-guide": {
20206
- "get": {
20207
- "operationId": "get_airbnb_checkin_guide",
20208
- "summary": "Get Airbnb check-in guide",
20209
- "description": "Return every published locale variant of an Airbnb listing's check-in guide. **Pure DB read** from `listings_airbnb_check_in_guides`. Pass `?locale=en` to filter to one locale (prefix match). Returns `404` when the listing has no Airbnb connection in this workspace.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
20210
- "tags": [
20211
- "Airbnb"
20212
- ],
20213
- "parameters": [
20214
- {
20215
- "name": "id",
20216
- "in": "path",
20217
- "required": true,
20218
- "schema": {
20219
- "type": "string"
20220
- },
20221
- "description": "Repull listing id (numeric string)."
20222
- },
20223
- {
20224
- "name": "locale",
20225
- "in": "query",
20226
- "required": false,
20227
- "schema": {
20228
- "type": "string",
20229
- "example": "en"
20230
- },
20231
- "description": "Filter to a single locale (prefix match, case-insensitive)."
20232
- }
20233
- ],
20234
- "responses": {
20235
- "200": {
20236
- "description": "Check-in guides",
20237
- "content": {
20238
- "application/json": {
20239
- "schema": {
20240
- "type": "object",
20241
- "required": [
20242
- "data",
20243
- "dataFreshness"
20244
- ],
20245
- "properties": {
20246
- "data": {
20247
- "type": "array",
20248
- "items": {
20249
- "type": "object",
20250
- "additionalProperties": true
20251
- }
20252
- },
20253
- "dataFreshness": {
20254
- "$ref": "#/components/schemas/AirbnbDataFreshness"
20255
- }
20256
- }
20257
- }
20258
- }
20259
- }
20260
- },
20261
- "401": {
20262
- "$ref": "#/components/responses/Unauthorized"
20263
- },
20264
- "403": {
20265
- "$ref": "#/components/responses/ListingInactive"
20266
- },
20267
- "404": {
20268
- "$ref": "#/components/responses/NotFound"
20269
- },
20270
- "422": {
20271
- "$ref": "#/components/responses/UnprocessableEntity"
20272
- },
20273
- "500": {
20274
- "$ref": "#/components/responses/InternalError"
20275
- }
20276
- }
20277
- },
20278
- "put": {
20279
- "operationId": "update_airbnb_checkin_guide",
20280
- "summary": "Upsert Airbnb check-in guide",
20281
- "description": "Upsert the check-in guide for one locale on an Airbnb listing. **Write-side** \u2014 calls Airbnb upstream; the DB mirror is reconciled by the sync worker once the upstream call returns. Target the locale with `?locale=en` (defaults to `en`). Requires a connected Airbnb host, else `404 no_connection`.\n\nReturns `403 listing_inactive` when the listing is inactive. An inactive listing keeps syncing, but cannot be read or changed through the API until it is activated.",
20282
- "tags": [
20283
- "Airbnb"
20284
- ],
20285
- "parameters": [
20286
- {
20287
- "name": "id",
20288
- "in": "path",
20289
- "required": true,
20290
- "schema": {
20291
- "type": "string"
20292
- },
20293
- "description": "Repull listing id (numeric string)."
20294
- },
20295
- {
20296
- "name": "locale",
20297
- "in": "query",
20298
- "required": false,
20299
- "schema": {
20300
- "type": "string",
20301
- "default": "en"
20302
- },
20303
- "description": "Locale to upsert. Defaults to `en`."
20304
- }
20305
- ],
20306
- "responses": {
20307
- "200": {
20308
- "description": "Check-in guide upserted"
20309
- },
20310
- "401": {
20311
- "$ref": "#/components/responses/Unauthorized"
20312
- },
20313
21677
  "403": {
20314
21678
  "$ref": "#/components/responses/AirbnbWriteForbidden"
20315
21679
  },
@@ -22100,7 +23464,7 @@
22100
23464
  ],
22101
23465
  "default": "active"
22102
23466
  },
22103
- "description": "Filter by listing status. Defaults to `active`. Pass `inactive` to list the listings you can activate, `archived` for archived ones, or `all` for every status. Inactive listings are returned with identity fields only \u2014 `id`, `name`, `status` and `channels` \u2014 and never with `address`, `content` or `details`; activate one to see the rest. The only field you can add to an inactive row is `thumbnailUrl`, via `?include=thumbnail`."
23467
+ "description": "Filter by listing status. Defaults to `active`. Pass `inactive` to list the listings you can activate, `archived` for archived ones, or `all` for every status. Inactive listings are returned with identity fields only \u2014 `id`, `name`, `status`, `inactiveReason` (`plan_limit`, `unlisted_on_airbnb` or `deactivated`), `address.city` and `channels` \u2014 and never with the street, `content` or `details`; activate one to see the rest. The only field you can add to an inactive row is `thumbnailUrl`, via `?include=thumbnail`."
22104
23468
  },
22105
23469
  {
22106
23470
  "name": "channel",
@@ -22779,10 +24143,27 @@
22779
24143
  "get": {
22780
24144
  "operationId": "list_booking_properties",
22781
24145
  "summary": "List Booking.com properties",
22782
- "description": "List every Booking.com property this workspace holds. Each property is returned ONCE, with the Repull listings mapped under it.\n\nA Booking.com property is a building; its rooms are what guests book, and each room is mapped to one Repull listing \u2014 so one property routinely carries many listings. `listings[].roomBookingId` is the Booking.com room id an ARI write takes.\n\nA property whose rooms are not mapped yet is still listed, with `mappingStatus: \"unmapped\"` and an empty `listings` array. That is a real mid-onboarding state, not an error: finish `POST /v1/connect/booking/map-rooms` and the listings appear. Such a property used to be dropped silently, which made a mapped-but-unreadable workspace indistinguishable from one with no Booking connection at all.\n\nInactive listings are left out of `listings`; they keep syncing and reappear once activated. Use `GET /v1/listings?status=inactive` to find them.",
24146
+ "description": "List every Booking.com property this workspace holds. Each property is returned ONCE, with the Repull listings mapped under it.\n\nA Booking.com property is a building; its rooms are what guests book, and each room is mapped to one Repull listing \u2014 so one property routinely carries many listings. `listings[].roomBookingId` is the Booking.com room id an ARI write takes.\n\nA property whose rooms are not mapped yet is still listed, with `mappingStatus: \"unmapped\"` and an empty `listings` array. That is a real mid-onboarding state, not an error: finish `POST /v1/connect/booking/map-rooms` and the listings appear. Such a property used to be dropped silently, which made a mapped-but-unreadable workspace indistinguishable from one with no Booking connection at all.\n\nInactive listings are left out of `listings` unless `?status=inactive|all` asks for them; they then appear with identity fields only (`listingId`, `name`, `city`, `status`, `inactiveReason`, room). `mappingStatus` counts every mapped listing, inactive ones included, so a property whose listings are all inactive is still `mapped`.",
22783
24147
  "tags": [
22784
24148
  "Booking.com"
22785
24149
  ],
24150
+ "parameters": [
24151
+ {
24152
+ "name": "status",
24153
+ "in": "query",
24154
+ "required": false,
24155
+ "schema": {
24156
+ "type": "string",
24157
+ "enum": [
24158
+ "active",
24159
+ "inactive",
24160
+ "all"
24161
+ ],
24162
+ "default": "active"
24163
+ },
24164
+ "description": "`active` (default) leaves inactive listings out. `inactive` returns only them and `all` returns both. An inactive listing comes back with identity fields only (ids, `name`, `city`, `status`, `inactiveReason`, its account), which is enough to show what can be activated. Every row carries `status`."
24165
+ }
24166
+ ],
22786
24167
  "responses": {
22787
24168
  "200": {
22788
24169
  "description": "Properties",
@@ -24181,10 +25562,27 @@
24181
25562
  "get": {
24182
25563
  "operationId": "list_vrbo_listings",
24183
25564
  "summary": "List VRBO listings",
24184
- "description": "List VRBO listings this workspace owns. VRBO is agency-model \u2014 Repull reads listings via the public iCal/HTTP feeds.\n\nInactive listings are left out; they keep syncing and reappear once activated. Use `GET /v1/listings?status=inactive` to find them.",
25565
+ "description": "List the Vrbo units linked to this workspace's listings, from the host's connected Vrbo account (host sign-in, beta).\n\nInactive listings are left out unless `?status=inactive|all` asks for them; they then come back with identity fields only (ids, `listingName`, `listingCity`, `status`, `inactiveReason`). They keep syncing and are complete again once activated.",
24185
25566
  "tags": [
24186
25567
  "VRBO"
24187
25568
  ],
25569
+ "parameters": [
25570
+ {
25571
+ "name": "status",
25572
+ "in": "query",
25573
+ "required": false,
25574
+ "schema": {
25575
+ "type": "string",
25576
+ "enum": [
25577
+ "active",
25578
+ "inactive",
25579
+ "all"
25580
+ ],
25581
+ "default": "active"
25582
+ },
25583
+ "description": "`active` (default) leaves inactive listings out. `inactive` returns only them and `all` returns both. An inactive listing comes back with identity fields only (ids, `name`, `city`, `status`, `inactiveReason`, its account), which is enough to show what can be activated. Every row carries `status`."
25584
+ }
25585
+ ],
24188
25586
  "responses": {
24189
25587
  "200": {
24190
25588
  "description": "Listings",
@@ -24203,7 +25601,7 @@
24203
25601
  "get": {
24204
25602
  "operationId": "list_vrbo_reservations",
24205
25603
  "summary": "List VRBO reservations",
24206
- "description": "Cursor-paginated list of VRBO reservations sourced from the public booking feed. Lag is typically 5-10 minutes vs. Airbnb / Booking.com. `?offset=` is accepted as a first-class alias for `?cursor=` (mutually exclusive; offset capped at 10000).\n\nReservations on inactive listings are left out (counts and cursors included); they keep syncing and reappear once the listing is activated.",
25604
+ "description": "Cursor-paginated list of Vrbo reservations imported from the host's connected Vrbo account (host sign-in, beta). The full normalised record \u2014 guest, price breakdown, policy \u2014 is on `GET /v1/reservations?platform=vrbo`. `?offset=` is accepted as a first-class alias for `?cursor=` (mutually exclusive; offset capped at 10000).\n\nReservations on inactive listings are left out (counts and cursors included); they keep syncing and reappear once the listing is activated.",
24207
25605
  "tags": [
24208
25606
  "VRBO"
24209
25607
  ],
@@ -27425,8 +28823,8 @@
27425
28823
  },
27426
28824
  "put": {
27427
28825
  "operationId": "updateAirbnbListingDetails",
27428
- "summary": "Update property type, room type, quiet hours or check-in method",
27429
- "description": "Change what kind of property the Airbnb listing is, when its quiet hours are, or how the guest gets in. Partial: only the fields you send are written. At least one required; an unknown field is refused by name rather than dropped.\n\nThis is the UPDATE path for fields that previously had none. `POST /v1/listings` accepts a `propertyType` when a listing is CREATED and nothing could change it afterwards, so a listing mis-typed at import stayed mis-typed; the check-in method was mirrored and never exposed at all.\n\n**A 200 does not by itself mean the change was applied.** `property_type_category`, `property_type_group` and `check_in_option` are among the attributes Airbnb locks on established listings: the write returns 200, and Airbnb applies nothing for the locked ones. The response reports `blockedFields` \u2014 the fields YOU sent that Airbnb dropped \u2014 and `blockedFields: []` is what a landed write looks like. `GET \u2026/details` reports the same list as `lockedFields` so you can check first.\n\nCanonical property type (the value Repull keeps and republishes) is set with `PUT /v1/listings/{id}/content` under `details`; this endpoint writes straight to Airbnb.\n\nSend `Idempotency-Key` to make a retry safe.",
28826
+ "summary": "Update property type, quiet hours, check-in method, house manual, directions or Wi-Fi",
28827
+ "description": "Change what kind of property the Airbnb listing is, when its quiet hours are, how the guest gets in (`check_in_option.instruction` is the arrival instructions), the house manual, directions to the property, or the Wi-Fi network and password. Partial: only the fields you send are written. At least one required; an unknown field is refused by name rather than dropped.\n\nThis is the UPDATE path for fields that previously had none. `POST /v1/listings` accepts a `propertyType` when a listing is CREATED and nothing could change it afterwards, so a listing mis-typed at import stayed mis-typed; the check-in method was mirrored and never exposed at all.\n\n**A 200 does not by itself mean the change was applied.** `property_type_category`, `property_type_group`, `check_in_option`, `house_manual`, `directions`, `wifi_network` and `wifi_password` are among the attributes Airbnb locks on some listings: the write returns 200, and Airbnb applies nothing for the locked ones. The response reports `blockedFields` \u2014 the fields YOU sent that Airbnb dropped \u2014 and `blockedFields: []` is what a landed write looks like. `GET \u2026/details` reports the same list as `lockedFields` so you can check first.\n\nCanonical property type (the value Repull keeps and republishes) is set with `PUT /v1/listings/{id}/content` under `details`; this endpoint writes straight to Airbnb.\n\nSend `Idempotency-Key` to make a retry safe.",
27430
28828
  "tags": [
27431
28829
  "Airbnb"
27432
28830
  ],
@@ -27568,7 +28966,7 @@
27568
28966
  "put": {
27569
28967
  "operationId": "updateAirbnbListingPermits",
27570
28968
  "summary": "Answer Airbnb permit questions",
27571
- "description": "Answer the regulatory permit questions for a listing \u2014 the licence or registration number a city requires to keep the listing up.\n\nRead the questions first with `GET \u2026/permits?source=live`. For each permit, pick one of its `flows[]` and send its `slug` as `flow_slug`; key every answer by the question's `answer_key`, and let the question's `type` decide the value field (`text_value`, `attestation_value`, `radio_value`, `date_value` or `selected_options_value`). Answers are forwarded verbatim \u2014 nothing is defaulted or inferred, because a wrong licence number can take a listing down in a regulated city.\n\nSend `Idempotency-Key`: a timeout here leaves you unable to tell \"never arrived\" from \"arrived, response lost\", and this is a compliance filing.\n\nAirbnb refusing the answers (an unknown `answer_key`, a malformed licence number) is `422 airbnb_rejected` carrying Airbnb's own reason. An expired or revoked Airbnb connection is `403 connection_reauth_required`.",
28969
+ "description": "Answer the regulatory permit questions for a listing \u2014 the licence or registration number a city requires to keep the listing up.\n\nRead the questions first with `GET \u2026/permits?source=live`. For each permit, pick one of its `flows[]` and send its `slug` as `flow_slug`; key every answer by the question's `answer_key`, and let the question's `type` decide the value field \u2014 `<type>_value`: `text_value`, `attestation_value`, `radio_value`, `dropdown_value`, `email_value`, `future_date_value`, `file_upload_value`, and so on. Answers are forwarded verbatim \u2014 nothing is defaulted or inferred, because a wrong licence number can take a listing down in a regulated city.\n\nSend `Idempotency-Key`: a timeout here leaves you unable to tell \"never arrived\" from \"arrived, response lost\", and this is a compliance filing.\n\nAirbnb refusing the answers (an unknown `answer_key`, a malformed licence number) is `422 airbnb_rejected` carrying Airbnb's own reason. An expired or revoked Airbnb connection is `403 connection_reauth_required`.\n\n**Changing and removing answers.** Airbnb has no call that deletes or withdraws a submitted registration, so neither does Repull, and at least one permit is required. To change an answer Airbnb marks `answer_editable`, submit the flow again with the new answers \u2014 the latest submission replaces the previous one. When a submission fails with status `failed_recoverable`, fix it and submit again; `failed` cannot be resubmitted. Hosts can also manage this at airbnb.com/verify-listing/{listing_id}.",
27572
28970
  "tags": [
27573
28971
  "Airbnb"
27574
28972
  ],
@@ -32016,7 +33414,8 @@
32016
33414
  "type": "object",
32017
33415
  "properties": {
32018
33416
  "listingId": {
32019
- "type": "integer"
33417
+ "type": "string",
33418
+ "description": "Repull listing id (numeric string, like every `*Id` on the wire)."
32020
33419
  },
32021
33420
  "total": {
32022
33421
  "type": "integer"
@@ -32079,7 +33478,7 @@
32079
33478
  "post": {
32080
33479
  "operationId": "cancel_reservation",
32081
33480
  "summary": "Cancel a reservation",
32082
- "description": "Cancels a reservation where it lives.\n\n- **Mews or Cloudbeds** (hotel-model PMS): cancelled in the PMS, then read back, so Repull and the PMS agree. No cancellation fee is charged.\n- **Direct, website or owner bookings**: cancelled in Repull \u2014 the nights are released and `reservation.cancelled` fires.\n- **A channel booking** (Airbnb, Booking.com, VRBO) or a booking owned by another PMS: `409 reservation_owned_by_channel`. Cancel it there; the cancellation reaches Repull with the next sync.\n\nCancelling an already-cancelled reservation is not an error: the response carries `alreadyCancelled: true`.\n\nReturns `403 listing_inactive` when the listing is inactive.",
33481
+ "description": "Cancels a reservation where it lives.\n\n- **A booking managed in a connected PMS** (Mews, Cloudbeds, Hostaway, Guesty, Beds24, BookingSync, Lodgify, Smoobu, Hospitable, iGMS): cancelled in the PMS, then read back, so Repull and the PMS agree. No cancellation fee is charged. Lodgify *declines* the booking rather than deleting it. **OwnerRez's API cannot cancel** \u2014 `422 pms_write_unsupported`; cancel it in OwnerRez. `GET /v1/listings/{id}` \u2192 `capabilities.reservations.cancel` says which applies.\n- **Direct, website or owner bookings**: cancelled in Repull \u2014 the nights are released and `reservation.cancelled` fires.\n- **A channel booking** (Airbnb, Booking.com, VRBO), including one that came in through a PMS: `409 reservation_owned_by_channel`. Cancel it on the channel; the cancellation reaches Repull with the next sync.\n\nCancelling an already-cancelled reservation is not an error: the response carries `alreadyCancelled: true`.\n\nPMS integrations other than Mews and Cloudbeds are verified against the vendor's API documentation only.\n\n`X-Account-Id` restricts the reservation to one connected account. Returns `403 listing_inactive` when the listing is inactive.",
32083
33482
  "tags": [
32084
33483
  "Reservations"
32085
33484
  ],
@@ -32092,6 +33491,19 @@
32092
33491
  "type": "integer"
32093
33492
  },
32094
33493
  "description": "Reservation id."
33494
+ },
33495
+ {
33496
+ "$ref": "#/components/parameters/IdempotencyKey"
33497
+ },
33498
+ {
33499
+ "name": "X-Account-Id",
33500
+ "in": "header",
33501
+ "required": false,
33502
+ "schema": {
33503
+ "type": "string",
33504
+ "example": "126"
33505
+ },
33506
+ "description": "Restrict the request to one connected account (a Repull connection id, `GET /v1/connect` \u2192 `id`). A listing or reservation outside that account answers `404 not_found`. Omit it to act workspace-wide."
32095
33507
  }
32096
33508
  ],
32097
33509
  "requestBody": {
@@ -32154,36 +33566,30 @@
32154
33566
  "description": "Present and true when the reservation was already cancelled."
32155
33567
  },
32156
33568
  "pms": {
32157
- "type": "object",
32158
- "description": "Present when the cancellation was made in a PMS.",
32159
- "properties": {
32160
- "provider": {
32161
- "type": "string",
32162
- "example": "mews"
32163
- },
32164
- "applied": {
32165
- "type": "array",
32166
- "items": {
32167
- "type": "string"
32168
- }
32169
- },
32170
- "errors": {
32171
- "type": "array",
32172
- "items": {
32173
- "type": "object",
32174
- "properties": {
32175
- "section": {
32176
- "type": "string"
32177
- },
32178
- "message": {
32179
- "type": "string"
32180
- },
32181
- "code": {
32182
- "type": "string"
32183
- }
32184
- }
32185
- }
32186
- }
33569
+ "$ref": "#/components/schemas/ReservationPmsOutcome"
33570
+ }
33571
+ }
33572
+ }
33573
+ }
33574
+ }
33575
+ },
33576
+ "400": {
33577
+ "description": "The body is not JSON.",
33578
+ "content": {
33579
+ "application/json": {
33580
+ "schema": {
33581
+ "$ref": "#/components/schemas/Error"
33582
+ },
33583
+ "examples": {
33584
+ "invalidJson": {
33585
+ "summary": "The body is not JSON",
33586
+ "value": {
33587
+ "error": {
33588
+ "code": "invalid_json",
33589
+ "message": "Request body is not valid JSON: Unexpected token",
33590
+ "fix": "Send a JSON object with `Content-Type: application/json`.",
33591
+ "docs_url": "https://repull.dev/docs/errors/invalid_json",
33592
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
32187
33593
  }
32188
33594
  }
32189
33595
  }
@@ -32194,14 +33600,199 @@
32194
33600
  "401": {
32195
33601
  "$ref": "#/components/responses/Unauthorized"
32196
33602
  },
33603
+ "403": {
33604
+ "description": "The listing is inactive (`listing_inactive`), or the PMS connection lacks write access to bookings (`connection_reauth_required`).",
33605
+ "content": {
33606
+ "application/json": {
33607
+ "schema": {
33608
+ "$ref": "#/components/schemas/Error"
33609
+ },
33610
+ "examples": {
33611
+ "reauthRequired": {
33612
+ "summary": "The grant lacks write access to bookings",
33613
+ "value": {
33614
+ "error": {
33615
+ "code": "connection_reauth_required",
33616
+ "message": "Beds24 needs to be reconnected before Repull can create reservations there: Beds24: token lacks scope write:bookings",
33617
+ "fix": "Reconnect Beds24 with an invite code that grants `write:bookings` (plus `bookings-personal` and `bookings-financial`), then retry.",
33618
+ "docs_url": "https://repull.dev/docs/errors/connection_reauth_required",
33619
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33620
+ "provider": "beds24",
33621
+ "reconnect": {
33622
+ "method": "POST",
33623
+ "path": "/v1/connect/beds24"
33624
+ }
33625
+ }
33626
+ }
33627
+ }
33628
+ }
33629
+ }
33630
+ }
33631
+ },
32197
33632
  "404": {
32198
- "$ref": "#/components/responses/NotFound"
33633
+ "description": "The reservation is not in this workspace (or not in the `X-Account-Id` account), or the PMS no longer has it.",
33634
+ "content": {
33635
+ "application/json": {
33636
+ "schema": {
33637
+ "$ref": "#/components/schemas/Error"
33638
+ },
33639
+ "examples": {
33640
+ "notFound": {
33641
+ "summary": "Not in this workspace (or not in the X-Account-Id account)",
33642
+ "value": {
33643
+ "error": {
33644
+ "code": "not_found",
33645
+ "message": "Reservation 215708 not found in this workspace.",
33646
+ "fix": "Verify the reservation exists in this workspace (`GET /v1/reservations`) and, with `X-Account-Id`, that its listing belongs to that account.",
33647
+ "docs_url": "https://repull.dev/docs/errors/not_found",
33648
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
33649
+ }
33650
+ }
33651
+ }
33652
+ }
33653
+ }
33654
+ }
32199
33655
  },
32200
33656
  "409": {
32201
- "description": "The reservation belongs to a channel or another PMS (`reservation_owned_by_channel`), or the PMS connection is gone (`no_connection`)."
33657
+ "description": "Cancel it elsewhere: a channel booking (`reservation_owned_by_channel`), part of a PMS group booking (`pms_group_booking`), writes off for the API (`pms_writes_off`), or no PMS connection (`no_connection`).",
33658
+ "content": {
33659
+ "application/json": {
33660
+ "schema": {
33661
+ "$ref": "#/components/schemas/Error"
33662
+ },
33663
+ "examples": {
33664
+ "ownedByChannel": {
33665
+ "summary": "A channel booking (incl. one relayed through a PMS)",
33666
+ "value": {
33667
+ "error": {
33668
+ "code": "reservation_owned_by_channel",
33669
+ "message": "This reservation came from airbnb through Hostaway. Change it on airbnb; the change reaches this workspace with the next sync.",
33670
+ "fix": "This booking belongs to a channel (Airbnb, Booking.com, Vrbo, \u2026). Change it on the channel; the change reaches Repull with the next sync and the matching webhook fires.",
33671
+ "docs_url": "https://repull.dev/docs/errors/reservation_owned_by_channel",
33672
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33673
+ "provider": "hostaway",
33674
+ "owner": "airbnb"
33675
+ }
33676
+ }
33677
+ },
33678
+ "groupBooking": {
33679
+ "summary": "Part of a group booking in the PMS",
33680
+ "value": {
33681
+ "error": {
33682
+ "code": "pms_group_booking",
33683
+ "message": "This stay is one room of a Cloudbeds group booking; change it in Cloudbeds.",
33684
+ "fix": "This stay is part of a group booking in Cloudbeds. Change or cancel group bookings in Cloudbeds; the change reaches Repull with the next sync.",
33685
+ "docs_url": "https://repull.dev/docs/errors/pms_group_booking",
33686
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33687
+ "provider": "cloudbeds"
33688
+ }
33689
+ }
33690
+ },
33691
+ "pmsWritesOff": {
33692
+ "summary": "The connection is set not to change bookings through the API",
33693
+ "value": {
33694
+ "error": {
33695
+ "code": "pms_writes_off",
33696
+ "message": "Bookings for this property are managed in its PMS, and this connection is set not to change them through the API. Nothing was sent to the PMS.",
33697
+ "fix": "Turn on `reservations.api` with `PATCH /v1/connect/{provider}/write-policy`, or make the change in the PMS.",
33698
+ "docs_url": "https://repull.dev/docs/connect/write-policy",
33699
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
33700
+ }
33701
+ }
33702
+ },
33703
+ "noConnection": {
33704
+ "summary": "The listing is managed in a PMS this workspace is no longer connected to",
33705
+ "value": {
33706
+ "error": {
33707
+ "code": "no_connection",
33708
+ "message": "Listing 4118 is managed in Hostaway, but there is no Hostaway connection.",
33709
+ "fix": "The listing is managed in Hostaway, but this workspace has no connection to it. Reconnect with `POST /v1/connect/hostaway`, then retry. Nothing was sent to the PMS.",
33710
+ "docs_url": "https://repull.dev/docs/errors/no_connection",
33711
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33712
+ "provider": "hostaway",
33713
+ "reconnect": {
33714
+ "method": "POST",
33715
+ "path": "/v1/connect/hostaway"
33716
+ }
33717
+ }
33718
+ }
33719
+ }
33720
+ }
33721
+ }
33722
+ }
32202
33723
  },
32203
33724
  "422": {
32204
- "$ref": "#/components/responses/UnprocessableEntity"
33725
+ "description": "The PMS's API cannot cancel it (`pms_write_unsupported` \u2014 OwnerRez), the PMS refused (`pms_rejected`), or the body is invalid (`invalid_params`).",
33726
+ "content": {
33727
+ "application/json": {
33728
+ "schema": {
33729
+ "$ref": "#/components/schemas/Error"
33730
+ },
33731
+ "examples": {
33732
+ "pmsWriteUnsupported": {
33733
+ "summary": "The PMS API cannot do this (here: OwnerRez has no cancel)",
33734
+ "value": {
33735
+ "error": {
33736
+ "code": "pms_write_unsupported",
33737
+ "message": "OwnerRez cannot cancel a booking through its API. Cancel it in OwnerRez.",
33738
+ "fix": "Cancel it in OwnerRez; the cancellation reaches Repull with the next sync.",
33739
+ "docs_url": "https://repull.dev/docs/errors/pms_write_unsupported",
33740
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33741
+ "provider": "ownerrez"
33742
+ }
33743
+ }
33744
+ },
33745
+ "pmsRejected": {
33746
+ "summary": "The PMS refused the content",
33747
+ "value": {
33748
+ "error": {
33749
+ "code": "pms_rejected",
33750
+ "message": "Hostaway refused to create the reservation: Hostaway: guestEmail is invalid",
33751
+ "fix": "Hostaway refused it; its reason is in `message`. Nothing was changed. Correct the request and retry with a NEW `Idempotency-Key`.",
33752
+ "docs_url": "https://repull.dev/docs/errors/pms_rejected",
33753
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33754
+ "provider": "hostaway",
33755
+ "pms": {
33756
+ "applied": [],
33757
+ "errors": [
33758
+ {
33759
+ "section": "reservation",
33760
+ "code": "rejected",
33761
+ "message": "Hostaway: guestEmail is invalid"
33762
+ }
33763
+ ]
33764
+ }
33765
+ }
33766
+ }
33767
+ }
33768
+ }
33769
+ }
33770
+ }
33771
+ },
33772
+ "502": {
33773
+ "description": "The PMS could not be reached (`pms_error`) \u2014 nothing was cancelled; retry.",
33774
+ "content": {
33775
+ "application/json": {
33776
+ "schema": {
33777
+ "$ref": "#/components/schemas/Error"
33778
+ },
33779
+ "examples": {
33780
+ "pmsError": {
33781
+ "summary": "The PMS could not be reached \u2014 retry with the same key",
33782
+ "value": {
33783
+ "error": {
33784
+ "code": "pms_error",
33785
+ "message": "Smoobu could not be reached to create the reservation: timeout of 30000ms exceeded",
33786
+ "fix": "Nothing was changed. Retry with the same idempotency key.",
33787
+ "docs_url": "https://repull.dev/docs/errors/pms_error",
33788
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
33789
+ "provider": "smoobu"
33790
+ }
33791
+ }
33792
+ }
33793
+ }
33794
+ }
33795
+ }
32205
33796
  }
32206
33797
  }
32207
33798
  }
@@ -33267,7 +34858,8 @@
33267
34858
  "type": "object",
33268
34859
  "properties": {
33269
34860
  "accountId": {
33270
- "type": "integer"
34861
+ "type": "string",
34862
+ "description": "The connection id (numeric string, like every `*Id` on the wire)."
33271
34863
  },
33272
34864
  "status": {
33273
34865
  "type": "string"
@@ -34059,6 +35651,295 @@
34059
35651
  }
34060
35652
  }
34061
35653
  }
35654
+ },
35655
+ "/v1/reservations/quote": {
35656
+ "post": {
35657
+ "operationId": "quote_reservation",
35658
+ "summary": "Quote a reservation in the PMS",
35659
+ "description": "Prices a stay and checks its availability **in the PMS that manages the listing**, without booking anything. It is the same check `POST /v1/reservations` makes before booking when no `totalPrice` is sent, so `available: true` with a `total` is what that create would be priced at (dates can still be taken in between).\n\n`available: false` is an answer, not an error: the PMS's reasons are in `restrictions` (minimum stay, closed to arrival, taken dates\u2026).\n\n- A listing **not managed in a PMS** answers `422 pms_not_linked`. Book it directly with `POST /v1/reservations`, or price it with `GET /v1/quotes`.\n- A PMS **without a quote API** (Mews, Cloudbeds, iGMS) answers `422 pms_write_unsupported`. You can still create the booking; on iGMS a `totalPrice` is required.\n\n`GET /v1/listings/{id}` \u2192 `capabilities.reservations.quote` says whether a listing can be quoted.\n\n### Per-PMS limits\n\n| PMS | create | change | cancel | quote | `totalPrice` | Limits |\n|---|---|---|---|---|---|---|\n| Mews | \u2713 | \u2713 | \u2713 | \u2013 | \u2713 | \u2014 |\n| Cloudbeds | \u2713 | \u2713 | \u2713 | \u2013 | \u2013 | Books at the rate plan's price; group bookings supported. |\n| Hostaway | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Direct-channel bookings only; a specific unit is refused; a date change keeps the booked total. |\n| Guesty | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Cancels direct and Vrbo bookings; other channel bookings are cancelled on the channel. |\n| Beds24 | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Needs the `write:bookings` scope; a multi-room property needs `unitId`. |\n| BookingSync | \u2713 | \u2713 | \u2713 | \u2713 | \u2713 | Needs `bookings_write`; fees and taxes are not itemized; no guest email. |\n| Lodgify | \u2713 | \u2713 | \u2713 (declines) | \u2713 | \u2713 | Cancel declines the booking; single-room bookings. |\n| Smoobu | \u2713 | \u2713 (no dates) | \u2713 | \u2713 | \u2713 | Dates cannot be changed through Smoobu's API \u2014 cancel and rebook, or change them in Smoobu. |\n| Hospitable | \u2713 | \u2713 | \u2713 | \u2713 (Direct plan) | \u2713 | Manual reservations only; needs `reservation:write`; adds no fees or taxes. |\n| iGMS | \u2713 | \u2713 | \u2713 | \u2013 | \u2713 (required) | iGMS direct bookings only; a price is required; no tentative holds. |\n| OwnerRez | \u2713 | \u2713 | \u2013 | \u2713 | \u2013 | No cancel through OwnerRez's API; priced by the property's own rates; needs the `full` scope. |\n\nEvery PMS except Cloudbeds refuses group bookings, and every vacation-rental PMS refuses to change or cancel a booking that came from a channel (Airbnb, Booking.com, Vrbo\u2026) \u2014 that is done on the channel.\n\n**Verification.** Mews and Cloudbeds were run end to end on their vendors' sandboxes. Every other PMS is **verified against the vendor's API documentation only** \u2014 no live account has been written to yet. `capabilities.reservations.verifiedAgainst` says which.\n\n`X-Account-Id` restricts the listing to one connected account. Returns `403 listing_inactive` when the listing is inactive.",
35660
+ "tags": [
35661
+ "Reservations"
35662
+ ],
35663
+ "parameters": [
35664
+ {
35665
+ "name": "X-Account-Id",
35666
+ "in": "header",
35667
+ "required": false,
35668
+ "schema": {
35669
+ "type": "string",
35670
+ "example": "126"
35671
+ },
35672
+ "description": "Restrict the request to one connected account (a Repull connection id, `GET /v1/connect` \u2192 `id`). A listing or reservation outside that account answers `404 not_found`. Omit it to act workspace-wide."
35673
+ }
35674
+ ],
35675
+ "requestBody": {
35676
+ "required": true,
35677
+ "content": {
35678
+ "application/json": {
35679
+ "schema": {
35680
+ "$ref": "#/components/schemas/ReservationQuoteRequest"
35681
+ },
35682
+ "examples": {
35683
+ "quote": {
35684
+ "summary": "Two adults and a child",
35685
+ "value": {
35686
+ "listingId": 4118,
35687
+ "checkIn": "2026-10-01",
35688
+ "checkOut": "2026-10-05",
35689
+ "adults": 2,
35690
+ "children": 1
35691
+ }
35692
+ }
35693
+ }
35694
+ }
35695
+ }
35696
+ },
35697
+ "responses": {
35698
+ "200": {
35699
+ "description": "The PMS's price and availability for the stay.",
35700
+ "content": {
35701
+ "application/json": {
35702
+ "schema": {
35703
+ "$ref": "#/components/schemas/ReservationQuoteResponse"
35704
+ },
35705
+ "examples": {
35706
+ "available": {
35707
+ "summary": "Bookable",
35708
+ "value": {
35709
+ "listingId": "4118",
35710
+ "provider": "hostaway",
35711
+ "checkIn": "2026-10-01",
35712
+ "checkOut": "2026-10-05",
35713
+ "available": true,
35714
+ "total": 880,
35715
+ "currency": "USD",
35716
+ "breakdown": {
35717
+ "accommodation": 720,
35718
+ "cleaningFee": 100,
35719
+ "taxes": 60
35720
+ },
35721
+ "restrictions": []
35722
+ }
35723
+ },
35724
+ "unavailable": {
35725
+ "summary": "Not bookable as asked",
35726
+ "value": {
35727
+ "listingId": "4118",
35728
+ "provider": "guesty",
35729
+ "checkIn": "2026-10-01",
35730
+ "checkOut": "2026-10-02",
35731
+ "available": false,
35732
+ "total": null,
35733
+ "currency": null,
35734
+ "breakdown": null,
35735
+ "restrictions": [
35736
+ "Guesty: minimum stay is 3 nights"
35737
+ ]
35738
+ }
35739
+ }
35740
+ }
35741
+ }
35742
+ }
35743
+ },
35744
+ "400": {
35745
+ "description": "The body is not JSON.",
35746
+ "content": {
35747
+ "application/json": {
35748
+ "schema": {
35749
+ "$ref": "#/components/schemas/Error"
35750
+ },
35751
+ "examples": {
35752
+ "invalidJson": {
35753
+ "summary": "The body is not JSON",
35754
+ "value": {
35755
+ "error": {
35756
+ "code": "invalid_json",
35757
+ "message": "Request body is not valid JSON: Unexpected token",
35758
+ "fix": "Send a JSON object with `Content-Type: application/json`.",
35759
+ "docs_url": "https://repull.dev/docs/errors/invalid_json",
35760
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
35761
+ }
35762
+ }
35763
+ }
35764
+ }
35765
+ }
35766
+ }
35767
+ },
35768
+ "401": {
35769
+ "$ref": "#/components/responses/Unauthorized"
35770
+ },
35771
+ "403": {
35772
+ "description": "The listing is inactive (`listing_inactive`), or the PMS connection must be reconnected (`connection_reauth_required`).",
35773
+ "content": {
35774
+ "application/json": {
35775
+ "schema": {
35776
+ "$ref": "#/components/schemas/Error"
35777
+ },
35778
+ "examples": {
35779
+ "reauthRequired": {
35780
+ "summary": "The grant lacks write access to bookings",
35781
+ "value": {
35782
+ "error": {
35783
+ "code": "connection_reauth_required",
35784
+ "message": "Beds24 needs to be reconnected before Repull can create reservations there: Beds24: token lacks scope write:bookings",
35785
+ "fix": "Reconnect Beds24 with an invite code that grants `write:bookings` (plus `bookings-personal` and `bookings-financial`), then retry.",
35786
+ "docs_url": "https://repull.dev/docs/errors/connection_reauth_required",
35787
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
35788
+ "provider": "beds24",
35789
+ "reconnect": {
35790
+ "method": "POST",
35791
+ "path": "/v1/connect/beds24"
35792
+ }
35793
+ }
35794
+ }
35795
+ }
35796
+ }
35797
+ }
35798
+ }
35799
+ },
35800
+ "404": {
35801
+ "description": "The listing is not in this workspace, or not in the `X-Account-Id` account.",
35802
+ "content": {
35803
+ "application/json": {
35804
+ "schema": {
35805
+ "$ref": "#/components/schemas/Error"
35806
+ },
35807
+ "examples": {
35808
+ "notFound": {
35809
+ "summary": "Not in this workspace (or not in the X-Account-Id account)",
35810
+ "value": {
35811
+ "error": {
35812
+ "code": "not_found",
35813
+ "message": "Listing 4118 not found in this workspace.",
35814
+ "fix": "Verify the listing exists in this workspace (`GET /v1/listings`) and, with `X-Account-Id`, that it belongs to that account.",
35815
+ "docs_url": "https://repull.dev/docs/errors/not_found",
35816
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
35817
+ }
35818
+ }
35819
+ }
35820
+ }
35821
+ }
35822
+ }
35823
+ },
35824
+ "409": {
35825
+ "description": "No PMS connection (`no_connection`), or writes are off for the API (`pms_writes_off`).",
35826
+ "content": {
35827
+ "application/json": {
35828
+ "schema": {
35829
+ "$ref": "#/components/schemas/Error"
35830
+ },
35831
+ "examples": {
35832
+ "noConnection": {
35833
+ "summary": "The listing is managed in a PMS this workspace is no longer connected to",
35834
+ "value": {
35835
+ "error": {
35836
+ "code": "no_connection",
35837
+ "message": "Listing 4118 is managed in Hostaway, but there is no Hostaway connection.",
35838
+ "fix": "The listing is managed in Hostaway, but this workspace has no connection to it. Reconnect with `POST /v1/connect/hostaway`, then retry. Nothing was sent to the PMS.",
35839
+ "docs_url": "https://repull.dev/docs/errors/no_connection",
35840
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
35841
+ "provider": "hostaway",
35842
+ "reconnect": {
35843
+ "method": "POST",
35844
+ "path": "/v1/connect/hostaway"
35845
+ }
35846
+ }
35847
+ }
35848
+ },
35849
+ "pmsWritesOff": {
35850
+ "summary": "The connection is set not to change bookings through the API",
35851
+ "value": {
35852
+ "error": {
35853
+ "code": "pms_writes_off",
35854
+ "message": "Bookings for this property are managed in its PMS, and this connection is set not to change them through the API. Nothing was sent to the PMS.",
35855
+ "fix": "Turn on `reservations.api` with `PATCH /v1/connect/{provider}/write-policy`, or make the change in the PMS.",
35856
+ "docs_url": "https://repull.dev/docs/connect/write-policy",
35857
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
35858
+ }
35859
+ }
35860
+ }
35861
+ }
35862
+ }
35863
+ }
35864
+ },
35865
+ "422": {
35866
+ "description": "Not managed in a PMS (`pms_not_linked`), a PMS without a quote API (`pms_write_unsupported`), or an invalid field (`invalid_params`).",
35867
+ "content": {
35868
+ "application/json": {
35869
+ "schema": {
35870
+ "$ref": "#/components/schemas/Error"
35871
+ },
35872
+ "examples": {
35873
+ "pmsNotLinked": {
35874
+ "summary": "The listing is not managed in a PMS",
35875
+ "value": {
35876
+ "error": {
35877
+ "code": "pms_not_linked",
35878
+ "message": "Listing 4118 is not managed in a PMS, so there is no PMS quote for it.",
35879
+ "fix": "Create the reservation directly; Repull prices it from the listing's own rates.",
35880
+ "docs_url": "https://repull.dev/docs/errors/pms_not_linked",
35881
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678"
35882
+ }
35883
+ }
35884
+ },
35885
+ "quoteUnsupported": {
35886
+ "summary": "The PMS has no quote API (Mews, Cloudbeds, iGMS)",
35887
+ "value": {
35888
+ "error": {
35889
+ "code": "pms_write_unsupported",
35890
+ "message": "iGMS cannot quote reservations through its API. Create the booking with a `totalPrice`.",
35891
+ "fix": "iGMS's API cannot quote this booking. Create it with a `totalPrice` instead.",
35892
+ "docs_url": "https://repull.dev/docs/errors/pms_write_unsupported",
35893
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
35894
+ "provider": "igms"
35895
+ }
35896
+ }
35897
+ },
35898
+ "invalidParams": {
35899
+ "summary": "A field failed validation",
35900
+ "value": {
35901
+ "error": {
35902
+ "code": "invalid_params",
35903
+ "message": "`checkOut` is invalid: `checkOut` (2026-10-01) must be after `checkIn` (2026-10-05) \u2014 a stay spans at least one night.",
35904
+ "fix": "Send the departure date as `YYYY-MM-DD`, after `checkIn`, e.g. `\"checkOut\": \"2026-10-05\"`.",
35905
+ "docs_url": "https://repull.dev/docs/errors/invalid_params",
35906
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
35907
+ "field": "checkOut",
35908
+ "value_received": "2026-10-01"
35909
+ }
35910
+ }
35911
+ }
35912
+ }
35913
+ }
35914
+ }
35915
+ },
35916
+ "502": {
35917
+ "description": "The PMS could not be reached (`pms_error`) \u2014 retry.",
35918
+ "content": {
35919
+ "application/json": {
35920
+ "schema": {
35921
+ "$ref": "#/components/schemas/Error"
35922
+ },
35923
+ "examples": {
35924
+ "pmsError": {
35925
+ "summary": "The PMS could not be reached \u2014 retry with the same key",
35926
+ "value": {
35927
+ "error": {
35928
+ "code": "pms_error",
35929
+ "message": "Smoobu could not be reached to create the reservation: timeout of 30000ms exceeded",
35930
+ "fix": "Nothing was changed. Retry with the same idempotency key.",
35931
+ "docs_url": "https://repull.dev/docs/errors/pms_error",
35932
+ "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
35933
+ "provider": "smoobu"
35934
+ }
35935
+ }
35936
+ }
35937
+ }
35938
+ }
35939
+ }
35940
+ }
35941
+ }
35942
+ }
34062
35943
  }
34063
35944
  }
34064
35945
  }