repull 0.2.15 → 0.2.16

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/repull/api/airbnb_api.rb +147 -52
  3. data/lib/repull/api/billing_api.rb +4 -4
  4. data/lib/repull/api/booking_com_api.rb +36 -27
  5. data/lib/repull/api/conversations_api.rb +398 -4
  6. data/lib/repull/api/guests_api.rb +2 -2
  7. data/lib/repull/api/listings_api.rb +4 -4
  8. data/lib/repull/api/reservations_api.rb +157 -6
  9. data/lib/repull/api/webhooks_api.rb +10 -3
  10. data/lib/repull/models/accept_reservation_request200_response.rb +321 -0
  11. data/lib/repull/models/account_created_event.rb +95 -31
  12. data/lib/repull/models/account_disconnected_event.rb +95 -31
  13. data/lib/repull/models/ai_operation_completed_event.rb +95 -31
  14. data/lib/repull/models/ai_operation_failed_event.rb +95 -31
  15. data/lib/repull/models/airbnb_reservation_action200_response.rb +157 -0
  16. data/lib/repull/models/airbnb_reservation_action_request.rb +248 -0
  17. data/lib/repull/models/booking_availability_update.rb +4 -20
  18. data/lib/repull/models/booking_availability_update_date_range.rb +193 -0
  19. data/lib/repull/models/booking_availability_update_request.rb +14 -3
  20. data/lib/repull/models/booking_pricing_rate_update.rb +32 -4
  21. data/lib/repull/models/booking_pricing_rate_update_date_range.rb +3 -0
  22. data/lib/repull/models/booking_pricing_rate_update_restrictions.rb +10 -10
  23. data/lib/repull/models/booking_pricing_response.rb +16 -4
  24. data/lib/repull/models/booking_pricing_update_response.rb +82 -18
  25. data/lib/repull/models/booking_property.rb +107 -36
  26. data/lib/repull/models/booking_property_listings_inner.rb +244 -0
  27. data/lib/repull/models/booking_rate_write_occupancy.rb +187 -0
  28. data/lib/repull/models/booking_rate_write_price_half.rb +188 -0
  29. data/lib/repull/models/booking_rate_write_restriction_half.rb +212 -0
  30. data/lib/repull/models/booking_rate_write_verification.rb +200 -0
  31. data/lib/repull/models/booking_rate_write_verification_row.rb +196 -0
  32. data/lib/repull/models/booking_restriction_request_row.rb +199 -0
  33. data/lib/repull/models/booking_restriction_verification.rb +208 -0
  34. data/lib/repull/models/booking_restriction_verification_row.rb +203 -0
  35. data/lib/repull/models/booking_restriction_verification_row_booking_value.rb +105 -0
  36. data/lib/repull/models/booking_restriction_verification_row_expected.rb +105 -0
  37. data/lib/repull/models/booking_rooms_rates_response.rb +41 -7
  38. data/lib/repull/models/booking_rooms_rates_response_rooms_inner.rb +12 -1
  39. data/lib/repull/models/booking_rooms_rates_response_rooms_inner_rates_inner.rb +1 -0
  40. data/lib/repull/models/booking_upstream_failure.rb +204 -0
  41. data/lib/repull/models/calendar_updated_event.rb +95 -31
  42. data/lib/repull/models/conversation_message_attachment.rb +28 -1
  43. data/lib/repull/models/create_airbnb_offer_request.rb +93 -8
  44. data/lib/repull/models/create_airbnb_offer_request_guest_details.rb +279 -0
  45. data/lib/repull/models/create_conversation_special_offer201_response.rb +293 -0
  46. data/lib/repull/models/create_conversation_special_offer201_response_guests.rb +189 -0
  47. data/lib/repull/models/create_conversation_special_offer_request.rb +263 -0
  48. data/lib/repull/models/create_conversation_special_offer_request_guests.rb +263 -0
  49. data/lib/repull/models/decline_reservation_request_request.rb +234 -0
  50. data/lib/repull/models/get_airbnb_offer200_response.rb +289 -0
  51. data/lib/repull/models/get_usage_summary200_response.rb +10 -1
  52. data/lib/repull/models/get_usage_summary200_response_limits.rb +11 -1
  53. data/lib/repull/models/get_usage_summary200_response_remaining.rb +11 -1
  54. data/lib/repull/models/get_usage_summary200_response_used.rb +10 -1
  55. data/lib/repull/models/get_usage_tier200_response.rb +10 -1
  56. data/lib/repull/models/get_usage_tier200_response_limits.rb +11 -1
  57. data/lib/repull/models/get_usage_tier200_response_remaining.rb +11 -1
  58. data/lib/repull/models/get_usage_tier200_response_used.rb +10 -1
  59. data/lib/repull/models/inquiry_created_event.rb +281 -0
  60. data/lib/repull/models/inquiry_created_payload.rb +185 -0
  61. data/lib/repull/models/inquiry_updated_event.rb +281 -0
  62. data/lib/repull/models/inquiry_updated_payload.rb +213 -0
  63. data/lib/repull/models/inquiry_webhook_object.rb +323 -0
  64. data/lib/repull/models/inquiry_webhook_object_expected_payout.rb +158 -0
  65. data/lib/repull/models/inquiry_webhook_object_guests.rb +188 -0
  66. data/lib/repull/models/list_airbnb_thread_messages200_response.rb +170 -0
  67. data/lib/repull/models/list_airbnb_thread_messages200_response_data_inner.rb +274 -0
  68. data/lib/repull/models/list_airbnb_thread_messages200_response_pagination.rb +158 -0
  69. data/lib/repull/models/list_inquiries200_response.rb +192 -0
  70. data/lib/repull/models/list_inquiries200_response_data_inner.rb +418 -0
  71. data/lib/repull/models/list_inquiries200_response_data_inner_expected_payout.rb +159 -0
  72. data/lib/repull/models/list_inquiries200_response_data_inner_guests.rb +188 -0
  73. data/lib/repull/models/listing_created_event.rb +95 -31
  74. data/lib/repull/models/listing_deleted_event.rb +95 -31
  75. data/lib/repull/models/listing_deleted_payload.rb +55 -21
  76. data/lib/repull/models/listing_reactivated_event.rb +281 -0
  77. data/lib/repull/models/listing_suspended_event.rb +281 -0
  78. data/lib/repull/models/listing_suspension_payload.rb +202 -0
  79. data/lib/repull/models/listing_updated_event.rb +95 -31
  80. data/lib/repull/models/listing_updated_payload.rb +50 -22
  81. data/lib/repull/models/listing_webhook_object.rb +291 -0
  82. data/lib/repull/models/listing_webhook_object_address.rb +168 -0
  83. data/lib/repull/models/listing_webhook_object_channels_inner.rb +184 -0
  84. data/lib/repull/models/message.rb +1 -0
  85. data/lib/repull/models/payment_completed_event.rb +95 -31
  86. data/lib/repull/models/payment_completed_payload.rb +46 -44
  87. data/lib/repull/models/payment_refunded_event.rb +95 -31
  88. data/lib/repull/models/payment_refunded_payload.rb +46 -44
  89. data/lib/repull/models/payment_webhook_object.rb +351 -0
  90. data/lib/repull/models/preapprove_conversation201_response.rb +253 -0
  91. data/lib/repull/models/preapprove_conversation_request.rb +150 -0
  92. data/lib/repull/models/replay_webhook_delivery_request.rb +150 -0
  93. data/lib/repull/models/repull_ping_event.rb +95 -31
  94. data/lib/repull/models/reservation.rb +22 -2
  95. data/lib/repull/models/reservation_alteration_created_event.rb +95 -31
  96. data/lib/repull/models/reservation_alteration_responded_event.rb +95 -31
  97. data/lib/repull/models/reservation_cancelled_event.rb +95 -31
  98. data/lib/repull/models/reservation_created_event.rb +95 -32
  99. data/lib/repull/models/reservation_message_received_event.rb +95 -31
  100. data/lib/repull/models/reservation_message_received_payload.rb +17 -4
  101. data/lib/repull/models/reservation_request_created_event.rb +281 -0
  102. data/lib/repull/models/reservation_request_created_payload.rb +224 -0
  103. data/lib/repull/models/reservation_request_updated_event.rb +281 -0
  104. data/lib/repull/models/reservation_request_updated_payload.rb +251 -0
  105. data/lib/repull/models/reservation_updated_event.rb +95 -31
  106. data/lib/repull/models/review_created_event.rb +95 -31
  107. data/lib/repull/models/review_responded_event.rb +95 -31
  108. data/lib/repull/models/send_airbnb_message201_response.rb +104 -0
  109. data/lib/repull/models/send_airbnb_message_request.rb +4 -20
  110. data/lib/repull/models/send_message_attachment.rb +225 -0
  111. data/lib/repull/models/send_message_part.rb +201 -0
  112. data/lib/repull/models/send_message_request.rb +44 -19
  113. data/lib/repull/models/send_message_response.rb +29 -5
  114. data/lib/repull/models/sent_attachment.rb +196 -0
  115. data/lib/repull/models/usage_quota_warning_event.rb +281 -0
  116. data/lib/repull/models/usage_quota_warning_payload.rb +224 -0
  117. data/lib/repull/models/usage_quota_warning_payload_top_operation.rb +167 -0
  118. data/lib/repull/models/webhook_event.rb +17 -3
  119. data/lib/repull/models/webhook_event_account.rb +171 -0
  120. data/lib/repull/models/webhook_event_type.rb +8 -1
  121. data/lib/repull/models/withdraw_conversation_special_offer200_response.rb +240 -0
  122. data/lib/repull/version.rb +1 -1
  123. data/lib/repull.rb +60 -0
  124. data/openapi/v1.json +19295 -13620
  125. data/scripts/regen.sh +1 -1
  126. metadata +62 -2
@@ -0,0 +1,104 @@
1
+ =begin
2
+ #Repull API
3
+
4
+ #The unified API for vacation rental tech. Connect to 50+ PMS platforms and 4 OTA channels through one REST API. Built-in AI operations for guest communication, pricing, and listing optimization. ## Designed for AI agents Every error response on this API includes machine-parseable fields so an LLM (Claude in MCP, Cursor, Cline, GPT, etc.) can self-recover without escalating to a human: - `error.code` — stable string identifier (e.g. `invalid_params`, `rate_limit_exceeded`) - `error.message` — human-readable cause - `error.fix` — exact recovery steps (e.g. \"Pass `check_in_after` as ISO 8601: `?check_in_after=2026-01-15`\") - `error.docs_url` — link to the canonical write-up at `https://repull.dev/docs/errors/{code}` - `error.request_id` — id to correlate with server-side logs - `error.field` / `error.value_received` / `error.valid_values` / `error.did_you_mean` — when the error is parameter-specific - `error.retry_after` — seconds to wait before retrying (rate-limit + transient upstream) `Access-Control-Expose-Headers` lists `x-request-id` and the `X-RateLimit-*` family so browsers can read them on cross-origin responses. ## Quick Start 1. Get an API key at https://repull.dev/dashboard 2. Connect a PMS: `POST /v1/connect/{provider}` 3. List properties: `GET /v1/properties` 4. Get reservations: `GET /v1/reservations` ## Authentication All requests require a Bearer token: ``` Authorization: Bearer sk_live_YOUR_API_KEY ``` ## Request Correlation (X-Request-ID) Every response carries an `X-Request-ID` header, e.g. `X-Request-ID: req_01HXY...`. Include this id in support tickets and bug reports — we can trace the full request lifecycle (auth, rate limit, handler, downstream calls, log row) from a single id. You may set the header on the inbound request to forward your own trace id; we will echo it back instead of generating a new one. Accepted format: `^[\\\\w.-]{1,128}$`. The id is also embedded in error envelopes as `request_id` so server-side log diffs work even when the response headers are stripped by an intermediate proxy. ## Rate Limits The public API enforces a per-API-key sliding-window rate limit on top of the per-tier monthly + daily-AI quotas. **Default policy:** 600 requests per 60 seconds, per API key. Sliding window — there is no fixed-minute boundary you can burst across. Every response includes: | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Requests permitted in the current window. | | `X-RateLimit-Remaining` | Requests left in the current window after this call. | | `X-RateLimit-Reset` | Unix epoch (seconds) when the next slot opens. | | `X-RateLimit-Policy` | Machine-readable policy descriptor, e.g. `600;w=60`. | | `Retry-After` | Seconds to wait before retrying. **Only present on 429 responses.** | **On 429 (rate_limit_exceeded):** the response body matches the standard error envelope with `code: \"rate_limit_exceeded\"`, plus `limit`, `window_seconds`, `retry_after`, and `request_id` fields. SDKs MUST honor `Retry-After` and use exponential backoff with jitter on subsequent retries — never a tight loop. Recommended backoff: ``` sleep_ms = (Retry-After * 1000) + random(0..250) ``` Monthly + daily-AI tier quotas (`free`, `starter`, `custom`) are enforced separately and also surface as 429s; they include `tier`, `scope`, and `resetsAt` fields. ## Plan Limits (402 — `listings_limit_exceeded`) The Repull API also enforces a per-tier cap on **active listings**: | Tier | Active listings cap | |---|---| | `free` | 3 | | `starter` | 50 | | `custom` | unlimited | When a customer's active-listing count is above their tier cap, the API returns **`402 Payment Required`** with `error.code = \"listings_limit_exceeded\"` on every route EXCEPT: - `/v1/health` — uptime probes are never gated. - `/v1/usage/*` — so dashboards can render the over-cap state. - Any `DELETE` — so the customer can trim listings to get back under the cap without paying. Unlike 429, 402 is NOT a \"wait and retry\" condition — `Retry-After` is not set. The only paths back to 200 are: 1. `DELETE` enough listings to come back under the cap, or 2. Upgrade at `https://repull.dev/dashboard/billing`. The server-side usage cache is 60s, so the first 200 after an upgrade may take up to a minute. The envelope mirrors `rate_limit_exceeded` for SDK ergonomics: `tier`, `limit`, `active_listings`, `upgrade_url`, plus the standard `code` / `message` / `fix` / `docs_url` / `request_id`.
5
+
6
+ The version of the OpenAPI document: 1.0.0
7
+ Contact: ivan@vanio.ai
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.22.0
10
+
11
+ =end
12
+
13
+ require 'date'
14
+ require 'time'
15
+
16
+ module Repull
17
+ module SendAirbnbMessage201Response
18
+ class << self
19
+ # List of class defined in oneOf (OpenAPI v3)
20
+ def openapi_one_of
21
+ [
22
+ :'Hash<String, Object>',
23
+ :'SendMessageResponse'
24
+ ]
25
+ end
26
+
27
+ # Builds the object
28
+ # @param [Mixed] Data to be matched against the list of oneOf items
29
+ # @return [Object] Returns the model or the data itself
30
+ def build(data)
31
+ # Go through the list of oneOf items and attempt to identify the appropriate one.
32
+ # Note:
33
+ # - We do not attempt to check whether exactly one item matches.
34
+ # - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
35
+ # due to the way the deserialization is made in the base_object template (it just casts without verifying).
36
+ # - TODO: scalar values are de facto behaving as if they were nullable.
37
+ # - TODO: logging when debugging is set.
38
+ openapi_one_of.each do |klass|
39
+ begin
40
+ next if klass == :AnyType # "nullable: true"
41
+ return find_and_cast_into_type(klass, data)
42
+ rescue # rescue all errors so we keep iterating even if the current item lookup raises
43
+ end
44
+ end
45
+
46
+ openapi_one_of.include?(:AnyType) ? data : nil
47
+ end
48
+
49
+ private
50
+
51
+ SchemaMismatchError = Class.new(StandardError)
52
+
53
+ # Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
54
+ def find_and_cast_into_type(klass, data)
55
+ return if data.nil?
56
+
57
+ case klass.to_s
58
+ when 'Boolean'
59
+ return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass)
60
+ when 'Float'
61
+ return data if data.instance_of?(Float)
62
+ when 'Integer'
63
+ return data if data.instance_of?(Integer)
64
+ when 'Time'
65
+ return Time.parse(data)
66
+ when 'Date'
67
+ return Date.iso8601(data)
68
+ when 'String'
69
+ return data if data.instance_of?(String)
70
+ when 'Object' # "type: object"
71
+ return data if data.instance_of?(Hash)
72
+ when /\AArray<(?<sub_type>.+)>\z/ # "type: array"
73
+ if data.instance_of?(Array)
74
+ sub_type = Regexp.last_match[:sub_type]
75
+ return data.map { |item| find_and_cast_into_type(sub_type, item) }
76
+ end
77
+ when /\AHash<String, (?<sub_type>.+)>\z/ # "type: object" with "additionalProperties: { ... }"
78
+ if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) }
79
+ sub_type = Regexp.last_match[:sub_type]
80
+ return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) }
81
+ end
82
+ else # model
83
+ const = Repull.const_get(klass)
84
+ if const
85
+ if const.respond_to?(:openapi_one_of) # nested oneOf model
86
+ model = const.build(data)
87
+ return model if model
88
+ else
89
+ # raise if data contains keys that are not known to the model
90
+ raise if const.respond_to?(:acceptable_attributes) && !(data.keys - const.acceptable_attributes).empty?
91
+ model = const.build_from_hash(data)
92
+ return model if model
93
+ end
94
+ end
95
+ end
96
+
97
+ raise # if no match by now, raise
98
+ rescue
99
+ raise SchemaMismatchError, "#{data} doesn't match the #{klass} type"
100
+ end
101
+ end
102
+ end
103
+
104
+ end
@@ -14,14 +14,15 @@ require 'date'
14
14
  require 'time'
15
15
 
16
16
  module Repull
17
+ # `message`, `mediaUrl`, or both.
17
18
  class SendAirbnbMessageRequest < ApiModelBase
18
- # Message body to send to the guest.
19
+ # Message body to send to the guest. Optional when `mediaUrl` is set (it is then sent as a separate message after the file).
19
20
  attr_accessor :message
20
21
 
21
- # Optional URL of an image/media attachment to send with the message.
22
+ # Public https URL of one image or video to send (JPEG/PNG/GIF/WebP/MP4/QuickTime, up to 10 MB). Repull uploads it to Airbnb for you.
22
23
  attr_accessor :media_url
23
24
 
24
- # Optional MIME/media type hint for `mediaUrl` (e.g. `image/jpeg`).
25
+ # Optional MIME type hint for `mediaUrl` (e.g. `image/jpeg`). The type is read from the file itself; this never overrides it.
25
26
  attr_accessor :media_type
26
27
 
27
28
  # Attribute mapping from ruby-style variable name to JSON key.
@@ -78,8 +79,6 @@ module Repull
78
79
 
79
80
  if attributes.key?(:'message')
80
81
  self.message = attributes[:'message']
81
- else
82
- self.message = nil
83
82
  end
84
83
 
85
84
  if attributes.key?(:'media_url')
@@ -96,10 +95,6 @@ module Repull
96
95
  def list_invalid_properties
97
96
  warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
98
97
  invalid_properties = Array.new
99
- if @message.nil?
100
- invalid_properties.push('invalid value for "message", message cannot be nil.')
101
- end
102
-
103
98
  invalid_properties
104
99
  end
105
100
 
@@ -107,20 +102,9 @@ module Repull
107
102
  # @return true if the model is valid
108
103
  def valid?
109
104
  warn '[DEPRECATED] the `valid?` method is obsolete'
110
- return false if @message.nil?
111
105
  true
112
106
  end
113
107
 
114
- # Custom attribute writer method with validation
115
- # @param [Object] message Value to be assigned
116
- def message=(message)
117
- if message.nil?
118
- fail ArgumentError, 'message cannot be nil'
119
- end
120
-
121
- @message = message
122
- end
123
-
124
108
  # Checks equality by comparing each attribute.
125
109
  # @param [Object] Object to be compared
126
110
  def ==(o)
@@ -0,0 +1,225 @@
1
+ =begin
2
+ #Repull API
3
+
4
+ #The unified API for vacation rental tech. Connect to 50+ PMS platforms and 4 OTA channels through one REST API. Built-in AI operations for guest communication, pricing, and listing optimization. ## Designed for AI agents Every error response on this API includes machine-parseable fields so an LLM (Claude in MCP, Cursor, Cline, GPT, etc.) can self-recover without escalating to a human: - `error.code` — stable string identifier (e.g. `invalid_params`, `rate_limit_exceeded`) - `error.message` — human-readable cause - `error.fix` — exact recovery steps (e.g. \"Pass `check_in_after` as ISO 8601: `?check_in_after=2026-01-15`\") - `error.docs_url` — link to the canonical write-up at `https://repull.dev/docs/errors/{code}` - `error.request_id` — id to correlate with server-side logs - `error.field` / `error.value_received` / `error.valid_values` / `error.did_you_mean` — when the error is parameter-specific - `error.retry_after` — seconds to wait before retrying (rate-limit + transient upstream) `Access-Control-Expose-Headers` lists `x-request-id` and the `X-RateLimit-*` family so browsers can read them on cross-origin responses. ## Quick Start 1. Get an API key at https://repull.dev/dashboard 2. Connect a PMS: `POST /v1/connect/{provider}` 3. List properties: `GET /v1/properties` 4. Get reservations: `GET /v1/reservations` ## Authentication All requests require a Bearer token: ``` Authorization: Bearer sk_live_YOUR_API_KEY ``` ## Request Correlation (X-Request-ID) Every response carries an `X-Request-ID` header, e.g. `X-Request-ID: req_01HXY...`. Include this id in support tickets and bug reports — we can trace the full request lifecycle (auth, rate limit, handler, downstream calls, log row) from a single id. You may set the header on the inbound request to forward your own trace id; we will echo it back instead of generating a new one. Accepted format: `^[\\\\w.-]{1,128}$`. The id is also embedded in error envelopes as `request_id` so server-side log diffs work even when the response headers are stripped by an intermediate proxy. ## Rate Limits The public API enforces a per-API-key sliding-window rate limit on top of the per-tier monthly + daily-AI quotas. **Default policy:** 600 requests per 60 seconds, per API key. Sliding window — there is no fixed-minute boundary you can burst across. Every response includes: | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Requests permitted in the current window. | | `X-RateLimit-Remaining` | Requests left in the current window after this call. | | `X-RateLimit-Reset` | Unix epoch (seconds) when the next slot opens. | | `X-RateLimit-Policy` | Machine-readable policy descriptor, e.g. `600;w=60`. | | `Retry-After` | Seconds to wait before retrying. **Only present on 429 responses.** | **On 429 (rate_limit_exceeded):** the response body matches the standard error envelope with `code: \"rate_limit_exceeded\"`, plus `limit`, `window_seconds`, `retry_after`, and `request_id` fields. SDKs MUST honor `Retry-After` and use exponential backoff with jitter on subsequent retries — never a tight loop. Recommended backoff: ``` sleep_ms = (Retry-After * 1000) + random(0..250) ``` Monthly + daily-AI tier quotas (`free`, `starter`, `custom`) are enforced separately and also surface as 429s; they include `tier`, `scope`, and `resetsAt` fields. ## Plan Limits (402 — `listings_limit_exceeded`) The Repull API also enforces a per-tier cap on **active listings**: | Tier | Active listings cap | |---|---| | `free` | 3 | | `starter` | 50 | | `custom` | unlimited | When a customer's active-listing count is above their tier cap, the API returns **`402 Payment Required`** with `error.code = \"listings_limit_exceeded\"` on every route EXCEPT: - `/v1/health` — uptime probes are never gated. - `/v1/usage/*` — so dashboards can render the over-cap state. - Any `DELETE` — so the customer can trim listings to get back under the cap without paying. Unlike 429, 402 is NOT a \"wait and retry\" condition — `Retry-After` is not set. The only paths back to 200 are: 1. `DELETE` enough listings to come back under the cap, or 2. Upgrade at `https://repull.dev/dashboard/billing`. The server-side usage cache is 60s, so the first 200 after an upgrade may take up to a minute. The envelope mirrors `rate_limit_exceeded` for SDK ergonomics: `tier`, `limit`, `active_listings`, `upgrade_url`, plus the standard `code` / `message` / `fix` / `docs_url` / `request_id`.
5
+
6
+ The version of the OpenAPI document: 1.0.0
7
+ Contact: ivan@vanio.ai
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.22.0
10
+
11
+ =end
12
+
13
+ require 'date'
14
+ require 'time'
15
+
16
+ module Repull
17
+ # A file to send, by URL. Repull downloads it (public `https://` only — no credentials in the URL, no private or internal addresses; redirects are followed and re-checked; 20 s timeout), reads its real type from the file's bytes, and keeps a durable copy. Nothing is sent to the guest until every file in the request has passed.
18
+ class SendMessageAttachment < ApiModelBase
19
+ # Public https URL of the file. A signed URL valid for a few minutes is fine.
20
+ attr_accessor :url
21
+
22
+ # Optional hint, e.g. `image/jpeg`. The type is read from the file itself; this never overrides it.
23
+ attr_accessor :content_type
24
+
25
+ # Optional display name. Defaults to the last segment of the URL.
26
+ attr_accessor :filename
27
+
28
+ # Attribute mapping from ruby-style variable name to JSON key.
29
+ def self.attribute_map
30
+ {
31
+ :'url' => :'url',
32
+ :'content_type' => :'contentType',
33
+ :'filename' => :'filename'
34
+ }
35
+ end
36
+
37
+ # Returns attribute mapping this model knows about
38
+ def self.acceptable_attribute_map
39
+ attribute_map
40
+ end
41
+
42
+ # Returns all the JSON keys this model knows about
43
+ def self.acceptable_attributes
44
+ acceptable_attribute_map.values
45
+ end
46
+
47
+ # Attribute type mapping.
48
+ def self.openapi_types
49
+ {
50
+ :'url' => :'String',
51
+ :'content_type' => :'String',
52
+ :'filename' => :'String'
53
+ }
54
+ end
55
+
56
+ # List of attributes with nullable: true
57
+ def self.openapi_nullable
58
+ Set.new([
59
+ ])
60
+ end
61
+
62
+ # Initializes the object
63
+ # @param [Hash] attributes Model attributes in the form of hash
64
+ def initialize(attributes = {})
65
+ if (!attributes.is_a?(Hash))
66
+ fail ArgumentError, "The input argument (attributes) must be a hash in `Repull::SendMessageAttachment` initialize method"
67
+ end
68
+
69
+ # check to see if the attribute exists and convert string to symbol for hash key
70
+ acceptable_attribute_map = self.class.acceptable_attribute_map
71
+ attributes = attributes.each_with_object({}) { |(k, v), h|
72
+ if (!acceptable_attribute_map.key?(k.to_sym))
73
+ fail ArgumentError, "`#{k}` is not a valid attribute in `Repull::SendMessageAttachment`. Please check the name to make sure it's valid. List of attributes: " + acceptable_attribute_map.keys.inspect
74
+ end
75
+ h[k.to_sym] = v
76
+ }
77
+
78
+ if attributes.key?(:'url')
79
+ self.url = attributes[:'url']
80
+ else
81
+ self.url = nil
82
+ end
83
+
84
+ if attributes.key?(:'content_type')
85
+ self.content_type = attributes[:'content_type']
86
+ end
87
+
88
+ if attributes.key?(:'filename')
89
+ self.filename = attributes[:'filename']
90
+ end
91
+ end
92
+
93
+ # Show invalid properties with the reasons. Usually used together with valid?
94
+ # @return Array for valid properties with the reasons
95
+ def list_invalid_properties
96
+ warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
97
+ invalid_properties = Array.new
98
+ if @url.nil?
99
+ invalid_properties.push('invalid value for "url", url cannot be nil.')
100
+ end
101
+
102
+ if @url.to_s.length > 2048
103
+ invalid_properties.push('invalid value for "url", the character length must be smaller than or equal to 2048.')
104
+ end
105
+
106
+ pattern = Regexp.new(/^https:\/\//)
107
+ if @url !~ pattern
108
+ invalid_properties.push("invalid value for \"url\", must conform to the pattern #{pattern}.")
109
+ end
110
+
111
+ if !@filename.nil? && @filename.to_s.length > 200
112
+ invalid_properties.push('invalid value for "filename", the character length must be smaller than or equal to 200.')
113
+ end
114
+
115
+ invalid_properties
116
+ end
117
+
118
+ # Check to see if the all the properties in the model are valid
119
+ # @return true if the model is valid
120
+ def valid?
121
+ warn '[DEPRECATED] the `valid?` method is obsolete'
122
+ return false if @url.nil?
123
+ return false if @url.to_s.length > 2048
124
+ return false if @url !~ Regexp.new(/^https:\/\//)
125
+ return false if !@filename.nil? && @filename.to_s.length > 200
126
+ true
127
+ end
128
+
129
+ # Custom attribute writer method with validation
130
+ # @param [Object] url Value to be assigned
131
+ def url=(url)
132
+ if url.nil?
133
+ fail ArgumentError, 'url cannot be nil'
134
+ end
135
+
136
+ if url.to_s.length > 2048
137
+ fail ArgumentError, 'invalid value for "url", the character length must be smaller than or equal to 2048.'
138
+ end
139
+
140
+ pattern = Regexp.new(/^https:\/\//)
141
+ if url !~ pattern
142
+ fail ArgumentError, "invalid value for \"url\", must conform to the pattern #{pattern}."
143
+ end
144
+
145
+ @url = url
146
+ end
147
+
148
+ # Custom attribute writer method with validation
149
+ # @param [Object] filename Value to be assigned
150
+ def filename=(filename)
151
+ if filename.nil?
152
+ fail ArgumentError, 'filename cannot be nil'
153
+ end
154
+
155
+ if filename.to_s.length > 200
156
+ fail ArgumentError, 'invalid value for "filename", the character length must be smaller than or equal to 200.'
157
+ end
158
+
159
+ @filename = filename
160
+ end
161
+
162
+ # Checks equality by comparing each attribute.
163
+ # @param [Object] Object to be compared
164
+ def ==(o)
165
+ return true if self.equal?(o)
166
+ self.class == o.class &&
167
+ url == o.url &&
168
+ content_type == o.content_type &&
169
+ filename == o.filename
170
+ end
171
+
172
+ # @see the `==` method
173
+ # @param [Object] Object to be compared
174
+ def eql?(o)
175
+ self == o
176
+ end
177
+
178
+ # Calculates hash code according to all attributes.
179
+ # @return [Integer] Hash code
180
+ def hash
181
+ [url, content_type, filename].hash
182
+ end
183
+
184
+ # Builds the object from hash
185
+ # @param [Hash] attributes Model attributes in the form of hash
186
+ # @return [Object] Returns the model itself
187
+ def self.build_from_hash(attributes)
188
+ return nil unless attributes.is_a?(Hash)
189
+ attributes = attributes.transform_keys(&:to_sym)
190
+ transformed_hash = {}
191
+ openapi_types.each_pair do |key, type|
192
+ if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil?
193
+ transformed_hash["#{key}"] = nil
194
+ elsif type =~ /\AArray<(.*)>/i
195
+ # check to ensure the input is an array given that the attribute
196
+ # is documented as an array but the input is not
197
+ if attributes[attribute_map[key]].is_a?(Array)
198
+ transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) }
199
+ end
200
+ elsif !attributes[attribute_map[key]].nil?
201
+ transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]])
202
+ end
203
+ end
204
+ new(transformed_hash)
205
+ end
206
+
207
+ # Returns the object in the form of hash
208
+ # @return [Hash] Returns the object in the form of hash
209
+ def to_hash
210
+ hash = {}
211
+ self.class.attribute_map.each_pair do |attr, param|
212
+ value = self.send(attr)
213
+ if value.nil?
214
+ is_nullable = self.class.openapi_nullable.include?(attr)
215
+ next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}"))
216
+ end
217
+
218
+ hash[param] = _to_hash(value)
219
+ end
220
+ hash
221
+ end
222
+
223
+ end
224
+
225
+ end
@@ -0,0 +1,201 @@
1
+ =begin
2
+ #Repull API
3
+
4
+ #The unified API for vacation rental tech. Connect to 50+ PMS platforms and 4 OTA channels through one REST API. Built-in AI operations for guest communication, pricing, and listing optimization. ## Designed for AI agents Every error response on this API includes machine-parseable fields so an LLM (Claude in MCP, Cursor, Cline, GPT, etc.) can self-recover without escalating to a human: - `error.code` — stable string identifier (e.g. `invalid_params`, `rate_limit_exceeded`) - `error.message` — human-readable cause - `error.fix` — exact recovery steps (e.g. \"Pass `check_in_after` as ISO 8601: `?check_in_after=2026-01-15`\") - `error.docs_url` — link to the canonical write-up at `https://repull.dev/docs/errors/{code}` - `error.request_id` — id to correlate with server-side logs - `error.field` / `error.value_received` / `error.valid_values` / `error.did_you_mean` — when the error is parameter-specific - `error.retry_after` — seconds to wait before retrying (rate-limit + transient upstream) `Access-Control-Expose-Headers` lists `x-request-id` and the `X-RateLimit-*` family so browsers can read them on cross-origin responses. ## Quick Start 1. Get an API key at https://repull.dev/dashboard 2. Connect a PMS: `POST /v1/connect/{provider}` 3. List properties: `GET /v1/properties` 4. Get reservations: `GET /v1/reservations` ## Authentication All requests require a Bearer token: ``` Authorization: Bearer sk_live_YOUR_API_KEY ``` ## Request Correlation (X-Request-ID) Every response carries an `X-Request-ID` header, e.g. `X-Request-ID: req_01HXY...`. Include this id in support tickets and bug reports — we can trace the full request lifecycle (auth, rate limit, handler, downstream calls, log row) from a single id. You may set the header on the inbound request to forward your own trace id; we will echo it back instead of generating a new one. Accepted format: `^[\\\\w.-]{1,128}$`. The id is also embedded in error envelopes as `request_id` so server-side log diffs work even when the response headers are stripped by an intermediate proxy. ## Rate Limits The public API enforces a per-API-key sliding-window rate limit on top of the per-tier monthly + daily-AI quotas. **Default policy:** 600 requests per 60 seconds, per API key. Sliding window — there is no fixed-minute boundary you can burst across. Every response includes: | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Requests permitted in the current window. | | `X-RateLimit-Remaining` | Requests left in the current window after this call. | | `X-RateLimit-Reset` | Unix epoch (seconds) when the next slot opens. | | `X-RateLimit-Policy` | Machine-readable policy descriptor, e.g. `600;w=60`. | | `Retry-After` | Seconds to wait before retrying. **Only present on 429 responses.** | **On 429 (rate_limit_exceeded):** the response body matches the standard error envelope with `code: \"rate_limit_exceeded\"`, plus `limit`, `window_seconds`, `retry_after`, and `request_id` fields. SDKs MUST honor `Retry-After` and use exponential backoff with jitter on subsequent retries — never a tight loop. Recommended backoff: ``` sleep_ms = (Retry-After * 1000) + random(0..250) ``` Monthly + daily-AI tier quotas (`free`, `starter`, `custom`) are enforced separately and also surface as 429s; they include `tier`, `scope`, and `resetsAt` fields. ## Plan Limits (402 — `listings_limit_exceeded`) The Repull API also enforces a per-tier cap on **active listings**: | Tier | Active listings cap | |---|---| | `free` | 3 | | `starter` | 50 | | `custom` | unlimited | When a customer's active-listing count is above their tier cap, the API returns **`402 Payment Required`** with `error.code = \"listings_limit_exceeded\"` on every route EXCEPT: - `/v1/health` — uptime probes are never gated. - `/v1/usage/*` — so dashboards can render the over-cap state. - Any `DELETE` — so the customer can trim listings to get back under the cap without paying. Unlike 429, 402 is NOT a \"wait and retry\" condition — `Retry-After` is not set. The only paths back to 200 are: 1. `DELETE` enough listings to come back under the cap, or 2. Upgrade at `https://repull.dev/dashboard/billing`. The server-side usage cache is 60s, so the first 200 after an upgrade may take up to a minute. The envelope mirrors `rate_limit_exceeded` for SDK ergonomics: `tier`, `limit`, `active_listings`, `upgrade_url`, plus the standard `code` / `message` / `fix` / `docs_url` / `request_id`.
5
+
6
+ The version of the OpenAPI document: 1.0.0
7
+ Contact: ivan@vanio.ai
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.22.0
10
+
11
+ =end
12
+
13
+ require 'date'
14
+ require 'time'
15
+
16
+ module Repull
17
+ # One channel message produced by the send. Airbnb carries one file per message and no text beside it, so text + 2 photos is 3 parts (files first, then the text). Booking.com carries every file on the one text message, so it is always 1 part.
18
+ class SendMessagePart < ApiModelBase
19
+ # Indexes into the request's `attachments` that this message carried.
20
+ attr_accessor :attachment_indexes
21
+
22
+ # Whether this message carried the text.
23
+ attr_accessor :has_text
24
+
25
+ attr_accessor :sent
26
+
27
+ attr_accessor :message_id
28
+
29
+ attr_accessor :external_message_id
30
+
31
+ # Why this part was not delivered.
32
+ attr_accessor :error
33
+
34
+ # Attribute mapping from ruby-style variable name to JSON key.
35
+ def self.attribute_map
36
+ {
37
+ :'attachment_indexes' => :'attachmentIndexes',
38
+ :'has_text' => :'hasText',
39
+ :'sent' => :'sent',
40
+ :'message_id' => :'messageId',
41
+ :'external_message_id' => :'externalMessageId',
42
+ :'error' => :'error'
43
+ }
44
+ end
45
+
46
+ # Returns attribute mapping this model knows about
47
+ def self.acceptable_attribute_map
48
+ attribute_map
49
+ end
50
+
51
+ # Returns all the JSON keys this model knows about
52
+ def self.acceptable_attributes
53
+ acceptable_attribute_map.values
54
+ end
55
+
56
+ # Attribute type mapping.
57
+ def self.openapi_types
58
+ {
59
+ :'attachment_indexes' => :'Array<Integer>',
60
+ :'has_text' => :'Boolean',
61
+ :'sent' => :'Boolean',
62
+ :'message_id' => :'String',
63
+ :'external_message_id' => :'String',
64
+ :'error' => :'String'
65
+ }
66
+ end
67
+
68
+ # List of attributes with nullable: true
69
+ def self.openapi_nullable
70
+ Set.new([
71
+ :'message_id',
72
+ :'external_message_id',
73
+ :'error'
74
+ ])
75
+ end
76
+
77
+ # Initializes the object
78
+ # @param [Hash] attributes Model attributes in the form of hash
79
+ def initialize(attributes = {})
80
+ if (!attributes.is_a?(Hash))
81
+ fail ArgumentError, "The input argument (attributes) must be a hash in `Repull::SendMessagePart` initialize method"
82
+ end
83
+
84
+ # check to see if the attribute exists and convert string to symbol for hash key
85
+ acceptable_attribute_map = self.class.acceptable_attribute_map
86
+ attributes = attributes.each_with_object({}) { |(k, v), h|
87
+ if (!acceptable_attribute_map.key?(k.to_sym))
88
+ fail ArgumentError, "`#{k}` is not a valid attribute in `Repull::SendMessagePart`. Please check the name to make sure it's valid. List of attributes: " + acceptable_attribute_map.keys.inspect
89
+ end
90
+ h[k.to_sym] = v
91
+ }
92
+
93
+ if attributes.key?(:'attachment_indexes')
94
+ if (value = attributes[:'attachment_indexes']).is_a?(Array)
95
+ self.attachment_indexes = value
96
+ end
97
+ end
98
+
99
+ if attributes.key?(:'has_text')
100
+ self.has_text = attributes[:'has_text']
101
+ end
102
+
103
+ if attributes.key?(:'sent')
104
+ self.sent = attributes[:'sent']
105
+ end
106
+
107
+ if attributes.key?(:'message_id')
108
+ self.message_id = attributes[:'message_id']
109
+ end
110
+
111
+ if attributes.key?(:'external_message_id')
112
+ self.external_message_id = attributes[:'external_message_id']
113
+ end
114
+
115
+ if attributes.key?(:'error')
116
+ self.error = attributes[:'error']
117
+ end
118
+ end
119
+
120
+ # Show invalid properties with the reasons. Usually used together with valid?
121
+ # @return Array for valid properties with the reasons
122
+ def list_invalid_properties
123
+ warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
124
+ invalid_properties = Array.new
125
+ invalid_properties
126
+ end
127
+
128
+ # Check to see if the all the properties in the model are valid
129
+ # @return true if the model is valid
130
+ def valid?
131
+ warn '[DEPRECATED] the `valid?` method is obsolete'
132
+ true
133
+ end
134
+
135
+ # Checks equality by comparing each attribute.
136
+ # @param [Object] Object to be compared
137
+ def ==(o)
138
+ return true if self.equal?(o)
139
+ self.class == o.class &&
140
+ attachment_indexes == o.attachment_indexes &&
141
+ has_text == o.has_text &&
142
+ sent == o.sent &&
143
+ message_id == o.message_id &&
144
+ external_message_id == o.external_message_id &&
145
+ error == o.error
146
+ end
147
+
148
+ # @see the `==` method
149
+ # @param [Object] Object to be compared
150
+ def eql?(o)
151
+ self == o
152
+ end
153
+
154
+ # Calculates hash code according to all attributes.
155
+ # @return [Integer] Hash code
156
+ def hash
157
+ [attachment_indexes, has_text, sent, message_id, external_message_id, error].hash
158
+ end
159
+
160
+ # Builds the object from hash
161
+ # @param [Hash] attributes Model attributes in the form of hash
162
+ # @return [Object] Returns the model itself
163
+ def self.build_from_hash(attributes)
164
+ return nil unless attributes.is_a?(Hash)
165
+ attributes = attributes.transform_keys(&:to_sym)
166
+ transformed_hash = {}
167
+ openapi_types.each_pair do |key, type|
168
+ if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil?
169
+ transformed_hash["#{key}"] = nil
170
+ elsif type =~ /\AArray<(.*)>/i
171
+ # check to ensure the input is an array given that the attribute
172
+ # is documented as an array but the input is not
173
+ if attributes[attribute_map[key]].is_a?(Array)
174
+ transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) }
175
+ end
176
+ elsif !attributes[attribute_map[key]].nil?
177
+ transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]])
178
+ end
179
+ end
180
+ new(transformed_hash)
181
+ end
182
+
183
+ # Returns the object in the form of hash
184
+ # @return [Hash] Returns the object in the form of hash
185
+ def to_hash
186
+ hash = {}
187
+ self.class.attribute_map.each_pair do |attr, param|
188
+ value = self.send(attr)
189
+ if value.nil?
190
+ is_nullable = self.class.openapi_nullable.include?(attr)
191
+ next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}"))
192
+ end
193
+
194
+ hash[param] = _to_hash(value)
195
+ end
196
+ hash
197
+ end
198
+
199
+ end
200
+
201
+ end