opendoor-partner-sdk-server-ruby 1.1.6.beta.89.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d37317c01e3cd9592d709cb3237fed1684aa0bcd5d878257b1ccefbf0b21e675
4
+ data.tar.gz: '0728067630fb3ce7a0d70f6231df246e689c46e45df9396c4a4e896dbbf07272'
5
+ SHA512:
6
+ metadata.gz: 8f553886988ebe99b974876a38e98dbf74bb0299386350fe8b42de3ebf888b16599f3be46f467b4b5953b7b5ea55c16a65395257e11ec38d46060d7d9364aee8
7
+ data.tar.gz: a7c722ac79a461f0177331b6864c6fc098b5567e87ae45488da4431ffa000fbd039eee6ed4e41366a3aac2cd69f3cebf8eea7d3b7c607c4bda798c3e8af38ae3
data/LICENSE.txt ADDED
@@ -0,0 +1,118 @@
1
+ PROPRIETARY LICENSE — OPENDOOR PARTNER SDK (DISTRIBUTED ARTIFACTS ONLY)
2
+
3
+ Copyright © 2026 Opendoor Labs. All rights reserved.
4
+
5
+ 1. Scope
6
+
7
+ This license applies solely to the bundled, compiled, or otherwise distributable
8
+ artifacts made available by Opendoor Labs on public package registries as part
9
+ of the "Opendoor Partner SDK" (the "Artifacts"). "Artifacts" include, without
10
+ limitation:
11
+
12
+ - The Ruby gem "opendoor-partner-sdk-server-ruby" distributed via RubyGems.org;
13
+ and
14
+ - The JavaScript/TypeScript npm packages published under the @opendoor/ scope
15
+ that are built from this repository (including, for example,
16
+ @opendoor/partner-sdk-server-js-core, @opendoor/partner-sdk-client-js-core,
17
+ @opendoor/partner-sdk-client-react, and @opendoor/partner-sdk-client-vue).
18
+
19
+ For clarity, this license does not grant any rights to any source code, build
20
+ scripts, or other materials that are not included in the Artifacts. All such
21
+ materials remain proprietary and confidential to Opendoor Labs.
22
+
23
+ 2. Prerequisite; Partner Agreement Controls
24
+
25
+ No license is granted under this notice unless you (or the entity on whose
26
+ behalf you act) have an active, fully executed written partner agreement with
27
+ Opendoor Labs Inc. ("Opendoor") governing your use of Opendoor's partner APIs
28
+ (the "Partner Agreement") and you are in compliance with the Partner Agreement.
29
+ Use of the Artifacts without an active Partner Agreement is not authorized.
30
+
31
+ If there is a conflict between this license and the Partner Agreement, the
32
+ Partner Agreement will control.
33
+
34
+ 3. Limited License Grant
35
+
36
+ Subject to Section 2, Opendoor grants you a limited, non-exclusive,
37
+ non-transferable, non-sublicensable, revocable license to: (a) download,
38
+ install, copy, and use the Artifacts for internal development, testing, and
39
+ operation; and (b) use the Artifacts solely to integrate your applications with
40
+ Opendoor partner APIs in accordance with Opendoor's published documentation and
41
+ the Partner Agreement.
42
+
43
+ You may reproduce the Artifacts as reasonably necessary for backup, caching,
44
+ CI/CD, and deployment, and you may distribute the Artifacts only as embedded or
45
+ included with your application for the authorized purpose above, and not on a
46
+ standalone basis.
47
+
48
+ 4. Restrictions
49
+
50
+ Except as expressly permitted in Section 3 or the Partner Agreement, you may
51
+ not (and may not permit any third party to): (a) copy, modify, translate, or
52
+ create derivative works of the Artifacts; (b) distribute, sublicense, sell,
53
+ lease, or otherwise make the Artifacts available to any third party on a
54
+ standalone basis; (c) reverse engineer, decompile, or disassemble the
55
+ Artifacts, except to the extent such restriction is prohibited by applicable
56
+ law; (d) remove, obscure, or alter any proprietary notices in the Artifacts; or
57
+ (e) use the Artifacts for any purpose other than the authorized integration with
58
+ Opendoor partner APIs. No rights are granted to Opendoor Labs' trademarks,
59
+ service marks, or logos under this license.
60
+
61
+ 5. Ownership
62
+
63
+ The Artifacts are licensed, not sold. Opendoor retains all right, title, and
64
+ interest in and to the Artifacts, including all intellectual property rights
65
+ therein.
66
+
67
+ 6. Disclaimer
68
+
69
+ THE ARTIFACTS ARE PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT WARRANTY OF ANY
70
+ KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING ANY IMPLIED
71
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND
72
+ NON-INFRINGEMENT.
73
+
74
+ 7. Limitation of Liability
75
+
76
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL OPENDOOR BE LIABLE FOR
77
+ ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY
78
+ DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, OR BUSINESS INTERRUPTION,
79
+ ARISING OUT OF OR RELATED TO THE ARTIFACTS OR THIS LICENSE, EVEN IF ADVISED OF
80
+ THE POSSIBILITY OF SUCH DAMAGES.
81
+
82
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, OPENDOOR'S TOTAL LIABILITY ARISING OUT
83
+ OF OR RELATED TO THE ARTIFACTS OR THIS LICENSE WILL NOT EXCEED THE LESSER OF
84
+ $100 OR THE AMOUNTS PAID BY YOU TO OPENDOOR UNDER THE PARTNER AGREEMENT IN THE
85
+ TWELVE (12) MONTHS PRECEDING THE EVENT GIVING RISE TO THE CLAIM.
86
+
87
+ 8. Termination
88
+
89
+ This license will terminate automatically upon: (a) any breach of this license;
90
+ (b) expiration or termination of the Partner Agreement; or (c) Opendoor's
91
+ written notice. Upon termination, you must immediately cease all use of the
92
+ Artifacts and destroy all copies in your possession or control, except that you
93
+ may retain copies solely as required to comply with applicable law or bona fide
94
+ internal record retention policies.
95
+
96
+ 9. Export and Sanctions Compliance
97
+
98
+ You represent and warrant that you are not (and are not acting on behalf of)
99
+ any person or entity that is the subject or target of sanctions administered or
100
+ enforced by the United States (including the U.S. Department of the Treasury's
101
+ Office of Foreign Assets Control), and that you will not use, export,
102
+ re-export, transfer, or release the Artifacts in violation of applicable export
103
+ control or sanctions laws and regulations.
104
+
105
+ 10. Governing Law; Venue
106
+
107
+ This license is governed by and construed in accordance with the laws of the
108
+ State of California, without regard to its conflict of law principles. Subject
109
+ to any dispute resolution provisions in the Partner Agreement, any dispute
110
+ arising out of or relating to the Artifacts or this license will be brought
111
+ exclusively in the state or federal courts located in California, and each
112
+ party consents to personal jurisdiction and venue there.
113
+
114
+ 11. Severability
115
+
116
+ If any provision of this license is held to be invalid or unenforceable, that
117
+ provision will be enforced to the maximum extent permissible, and the remaining
118
+ provisions will remain in full force and effect.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Opendoor Partner Server Ruby SDK
2
+
3
+ Standalone Ruby port of `packages/server-js-core`.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem "opendoor-partner-sdk-server-ruby"
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require "opendoor/partner_sdk"
15
+
16
+ client = Opendoor::PartnerSdk::Client.new(
17
+ api_key: ENV.fetch("OPENDOOR_API_KEY"),
18
+ environment: :staging
19
+ )
20
+
21
+ offer = client.create_offer(
22
+ data: {
23
+ address: {
24
+ street1: "123 Main St",
25
+ city: "Austin",
26
+ state: "TX",
27
+ postalCode: "78701"
28
+ }
29
+ }
30
+ )
31
+ ```
32
+
33
+ The `timeout:` keyword accepts **milliseconds** (default `30_000`), matching `@opendoor/partner-sdk-server-js-core`.
34
+
35
+ ## API Methods
36
+
37
+ - `get_address_suggestions(query:)`
38
+ - `create_offer(data:)`
39
+ - `update_offer(offer_id:, data:)`
40
+ - `get_offer(offer_id:)`
41
+ - `get_offer_with_sell_direct_pricing(offer_id:)`
42
+ - `check_address_unit(address:)`
43
+ - `get_address_details(address:, tracking_id: nil)`
44
+ - `get_homebuilders`
45
+ - `get_assessment_slots(offer_id:)`
46
+ - `get_home_detail(opendoor_offer_request_id:)`
47
+
48
+ ## Errors
49
+
50
+ - `Opendoor::PartnerSdk::ValidationError` for local input validation
51
+ - `Opendoor::PartnerSdk::ApiError` for network/API failures, GraphQL errors, and invalid JSON (`code: "PARSE_ERROR"`)
52
+
53
+ ## License
54
+
55
+ The Gem is **proprietary** to Opendoor Labs. Opendoor distributes it only as the **bundled package** on RubyGems.org under the terms in [`LICENSE.txt`](LICENSE.txt). Development sources and the broader codebase are **not** offered under an open-source license. Use is limited to integrating with Opendoor partner APIs as described in Opendoor documentation.
56
+
57
+ ## Publishing (maintainers)
58
+
59
+ CI builds and tests this gem on every PR (`ci.yml`). Releases mirror the JS packages:
60
+
61
+ | Trigger | Workflow | What gets published |
62
+ |--------|----------|----------------------|
63
+ | GitHub **Release** published (semver tag, e.g. `v1.2.3`) | [`publish.yml`](../../.github/workflows/publish.yml) — job `publish-rubygem` | Release gem `opendoor-partner-sdk-server-ruby` at that version |
64
+ | Push to **`main`** | [`publish-next.yml`](../../.github/workflows/publish-next.yml) — job `publish-rubygem-next` | Prerelease gem `X.Y.Z.beta.{run}.{attempt}` |
65
+
66
+ ### GitHub Actions secrets
67
+
68
+ | Secret | Required for | Where to get it |
69
+ |--------|----------------|-----------------|
70
+ | **`RUBYGEMS_API_KEY`** | `gem push` to rubygems.org | [rubygems.org → Edit profile → API access](https://rubygems.org/profile/edit). Create a key with permission to **push** gems for this gem name. |
71
+
72
+ Add the secret in the repo: **Settings → Secrets and variables → Actions → New repository secret**. Name must be exactly `RUBYGEMS_API_KEY`.
73
+
74
+ The npm workflows continue to use **`NPM_TOKEN`** for JS packages only; Ruby publishing does not use it.
75
+
76
+ ### First-time gem name
77
+
78
+ The owning RubyGems account must be authorized to publish `opendoor-partner-sdk-server-ruby` (create the gem once or accept ownership transfer per RubyGems policy).
@@ -0,0 +1,187 @@
1
+ module Opendoor
2
+ module PartnerSdk
3
+ module AnswerKeyMapping
4
+ DOT_TO_CAMEL = {
5
+ "home.bedrooms" => "homeBedrooms",
6
+ "home.bathrooms.full" => "homeBathroomsFull",
7
+ "home.bathrooms.half" => "homeBathroomsPartial",
8
+ "home.above_grade_sq_ft" => "homeAboveGradeSqFt",
9
+ "home.dwelling_type" => "homeDwellingType",
10
+ "home.year_built" => "homeYearBuilt",
11
+ "home.exterior_stories" => "homeExteriorStories",
12
+ "home.entry_type" => "homeEntryType",
13
+ "home.covered_parking_type" => "homeCoveredParkingType",
14
+ "home.garage_spaces" => "homeGarageSpaces",
15
+ "home.carport_spaces" => "homeCarportSpaces",
16
+ "home.pool_type" => "homePoolType",
17
+ "home.has_basement" => "homeHasBasement",
18
+ "home.basement_finished_sq_ft" => "homeBasementFinishedSqFt",
19
+ "home.basement_unfinished_sq_ft" => "homeBasementUnfinishedSqFt",
20
+ "home.kitchen_counter_type" => "homeKitchenCounterType",
21
+ "home.hoa" => "homeHoa",
22
+ "home.hoa_fees" => "homeHoaFees",
23
+ "home.has_upgrades" => "homeHasUpgrades",
24
+ "home.kitchen_seller_score" => "homeKitchenCondition",
25
+ "home.bathroom_seller_score" => "homeBathroomCondition",
26
+ "home.living_room_seller_score" => "homeLivingRoomCondition",
27
+ "home.exterior_seller_score" => "homeExteriorCondition",
28
+ "home.eligibility_criteria.leased_solar_panels" => "homeEligibilityCriteriaLeasedSolarPanels",
29
+ "home.eligibility_criteria.known_foundation_issues" => "homeEligibilityCriteriaKnownFoundationIssues",
30
+ "home.eligibility_criteria.fire_damage" => "homeEligibilityCriteriaFireDamage",
31
+ "home.eligibility_criteria.well_water" => "homeEligibilityCriteriaWellWater",
32
+ "home.eligibility_criteria.septic" => "homeEligibilityCriteriaSeptic",
33
+ "home.eligibility_criteria.asbestos_siding" => "homeEligibilityCriteriaAsbestosSiding",
34
+ "home.eligibility_criteria.livestock" => "homeEligibilityCriteriaLivestock",
35
+ "home.eligibility_criteria.mobile_manufactured_home" => "homeEligibilityCriteriaMobileManufacturedHome",
36
+ "seller.full_name" => "sellerFullName",
37
+ "seller.email" => "sellerEmail",
38
+ "seller.phone_number" => "sellerPhoneNumber",
39
+ "seller.sms_opt_in" => "sellerSmsOptIn",
40
+ "seller.relation_to_owner" => "sellerRelationToOwner",
41
+ "seller.relation_to_owner.other" => "sellerRelationToOwnerOther",
42
+ "seller.sale_timeline" => "sellerSaleTimeline",
43
+ "seller.working_with_home_builder" => "sellerWorkingWithHomeBuilder",
44
+ "seller.home_builder" => "sellerHomeBuilder",
45
+ "seller.home_builder_email" => "sellerHomeBuilderEmail",
46
+ "seller.home_builder_community" => "sellerHomeBuilderCommunity"
47
+ }.freeze
48
+
49
+ SKIP_KEYS = ["offerId"].freeze
50
+
51
+ NUMERIC_KEYS = [
52
+ "home.bedrooms",
53
+ "home.bathrooms.full",
54
+ "home.bathrooms.half",
55
+ "home.above_grade_sq_ft",
56
+ "home.exterior_stories",
57
+ "home.year_built",
58
+ "home.garage_spaces",
59
+ "home.carport_spaces",
60
+ "home.hoa_fees",
61
+ "home.basement_finished_sq_ft",
62
+ "home.basement_unfinished_sq_ft"
63
+ ].freeze
64
+
65
+ EXPERIMENTAL_KEYS = [
66
+ "home.mortgage_balance",
67
+ "home.lot_sq_ft",
68
+ "home.upper_level_sq_ft",
69
+ "home.basement_sq_ft"
70
+ ].freeze
71
+
72
+ POOL_TYPE_NORMALIZE = {
73
+ "none" => "no_pool",
74
+ "community" => "community_pool"
75
+ }.freeze
76
+
77
+ module_function
78
+
79
+ def transform_answers_to_offer_input(answers)
80
+ offer_input = {}
81
+ experimental = []
82
+
83
+ answers.each do |raw_key, raw_value|
84
+ key = raw_key.to_s
85
+ next if raw_value.nil? || SKIP_KEYS.include?(key)
86
+
87
+ value = normalize_pool_type(key, raw_value)
88
+
89
+ if key == "home.hoa_type"
90
+ values = value.is_a?(Array) ? value : [value]
91
+ next if values.length == 1 && values.first == "none"
92
+
93
+ offer_input["homeHoaTypeAgeRestrictedCommunity"] = values.include?("age_restricted_community")
94
+ offer_input["homeHoaTypeGatedCommunity"] = values.include?("gated_community")
95
+ next
96
+ end
97
+
98
+ if key == "home.eligibility_criteria"
99
+ map_eligibility_criteria(offer_input, value)
100
+ next
101
+ end
102
+
103
+ if key == "home.hoa_type.guarded_gated_community"
104
+ offer_input["homeHoaTypeGuardedGatedCommunity"] = value == "has_guard"
105
+ next
106
+ end
107
+
108
+ camel_key = DOT_TO_CAMEL[key]
109
+ if camel_key
110
+ offer_input[camel_key] = coerce_mapped_value(key, value)
111
+ elsif experimental_key?(key)
112
+ experimental_value = to_experimental_entry(key, value)
113
+ experimental << experimental_value if experimental_value
114
+ else
115
+ offer_input[key] = value
116
+ end
117
+ end
118
+
119
+ offer_input["experimental"] = experimental unless experimental.empty?
120
+ offer_input
121
+ end
122
+
123
+ def normalize_pool_type(key, value)
124
+ return value unless key == "home.pool_type"
125
+ return value unless value.is_a?(String)
126
+
127
+ POOL_TYPE_NORMALIZE.fetch(value, value)
128
+ end
129
+
130
+ def map_eligibility_criteria(offer_input, value)
131
+ values = value.is_a?(Array) ? value : []
132
+ criteria_map = {
133
+ "leased_solar_panels" => "homeEligibilityCriteriaLeasedSolarPanels",
134
+ "known_foundation_issues" => "homeEligibilityCriteriaKnownFoundationIssues",
135
+ "fire_damage" => "homeEligibilityCriteriaFireDamage",
136
+ "well_water" => "homeEligibilityCriteriaWellWater",
137
+ "septic" => "homeEligibilityCriteriaSeptic",
138
+ "asbestos_siding" => "homeEligibilityCriteriaAsbestosSiding",
139
+ "livestock" => "homeEligibilityCriteriaLivestock",
140
+ "mobile_manufactured_home" => "homeEligibilityCriteriaMobileManufacturedHome",
141
+ "unique_ownership_structure" => "homeEligibilityCriteriaUniqueOwnershipStructure",
142
+ "below_market_rate_ownership" => "homeEligibilityCriteriaBelowMarketRateOwnership",
143
+ "rent_controlled_tenant_occupied" => "homeEligibilityCriteriaRentControlledTenantOccupied"
144
+ }
145
+
146
+ criteria_map.each do |criteria_value, graphql_field|
147
+ offer_input[graphql_field] = values.include?(criteria_value)
148
+ end
149
+ end
150
+
151
+ def coerce_mapped_value(key, value)
152
+ return true if value == "yes"
153
+ return false if value == "no"
154
+ return true if value == "true"
155
+ return false if value == "false"
156
+
157
+ if value.is_a?(String) && !value.empty? && NUMERIC_KEYS.include?(key) && numeric_string?(value)
158
+ return value.include?(".") ? value.to_f : value.to_i
159
+ end
160
+
161
+ value
162
+ end
163
+
164
+ def numeric_string?(value)
165
+ !!Float(value)
166
+ rescue ArgumentError, TypeError
167
+ false
168
+ end
169
+
170
+ def experimental_key?(key)
171
+ key.start_with?("home.condition.") ||
172
+ key.start_with?("home.features.") ||
173
+ EXPERIMENTAL_KEYS.include?(key)
174
+ end
175
+
176
+ def to_experimental_entry(key, value)
177
+ if value.is_a?(Array)
178
+ { "questionKey" => key, "stringValues" => value }
179
+ elsif value.is_a?(Numeric)
180
+ { "questionKey" => key, "doubleValue" => value }
181
+ elsif value == true || value == false
182
+ { "questionKey" => key, "stringValues" => [value ? "true" : "false"] }
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,412 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "securerandom"
4
+ require "set"
5
+ require "uri"
6
+
7
+ module Opendoor
8
+ module PartnerSdk
9
+ class Client
10
+ VALID_OFFER_INPUT_FIELDS = %w[
11
+ sellerEmail sellerFullName sellerPhoneNumber sellerRelationToOwner sellerWorkingWithHomeBuilder
12
+ sellerSaleTimeline homeDwellingType homeBedrooms homeBathroomsFull homeBathroomsPartial homeYearBuilt
13
+ homeExteriorStories homeAboveGradeSqFt homeCoveredParkingType homeGarageSpaces homeCarportSpaces
14
+ homePoolType homeHasBasement homeBasementFinishedSqFt homeBasementUnfinishedSqFt homeKitchenCounterType
15
+ homeHoa homeHoaTypeAgeRestrictedCommunity homeHoaTypeGatedCommunity homeHoaTypeGuardedGatedCommunity
16
+ homeEntryType homeKitchenCondition homeBathroomCondition homeLivingRoomCondition homeExteriorCondition
17
+ homeEligibilityCriteriaLeasedSolarPanels homeEligibilityCriteriaKnownFoundationIssues homeEligibilityCriteriaFireDamage
18
+ homeEligibilityCriteriaWellWater homeEligibilityCriteriaSeptic homeEligibilityCriteriaAsbestosSiding
19
+ homeEligibilityCriteriaLivestock homeEligibilityCriteriaMobileManufacturedHome homeEligibilityCriteriaUniqueOwnershipStructure
20
+ homeEligibilityCriteriaBelowMarketRateOwnership homeEligibilityCriteriaRentControlledTenantOccupied homeHoaFees
21
+ homeHasUpgrades sellerSmsOptIn correlationId experimental
22
+ ].to_set.freeze
23
+
24
+ ENUM_FIELDS = %w[
25
+ homeDwellingType homeCoveredParkingType homePoolType homeEntryType homeKitchenCounterType
26
+ homeKitchenCondition homeBathroomCondition homeLivingRoomCondition homeExteriorCondition sellerRelationToOwner
27
+ ].to_set.freeze
28
+
29
+ ENDPOINTS = {
30
+ production: {
31
+ api_endpoint: "https://partner.opendoor.com/api/graphql",
32
+ address_endpoint: "https://www.opendoor.com",
33
+ gateway_endpoint: "https://graphql.opendoor.com/api/graphql"
34
+ },
35
+ staging: {
36
+ api_endpoint: "https://partner.simplersell.com/api/graphql",
37
+ address_endpoint: "https://demo.simplersell.com",
38
+ gateway_endpoint: "https://graphql.simplersell.com/api/graphql"
39
+ }
40
+ }.freeze
41
+
42
+ # Matches server-js-core: request timeout in milliseconds (see OpendoorClientConfig.timeout).
43
+ DEFAULT_TIMEOUT_MS = 30_000
44
+
45
+ def initialize(api_key:, environment: :staging, timeout: DEFAULT_TIMEOUT_MS, debug: false, api_endpoint: nil, address_endpoint: nil, gateway_endpoint: nil)
46
+ raise ValidationError.new("apiKey is required", "apiKey") if blank?(api_key)
47
+
48
+ env = environment.to_sym
49
+ endpoints = ENDPOINTS.fetch(env)
50
+
51
+ @api_key = api_key
52
+ @api_endpoint = api_endpoint || endpoints[:api_endpoint]
53
+ @address_endpoint = address_endpoint || endpoints[:address_endpoint]
54
+ @gateway_endpoint = gateway_endpoint || endpoints[:gateway_endpoint]
55
+ @timeout_ms = timeout
56
+ # Net::HTTP expects seconds (fractional allowed); keep parity with JS ms default.
57
+ @timeout_seconds = [@timeout_ms / 1000.0, 0.001].max
58
+ @debug = debug
59
+ end
60
+
61
+ def get_address_suggestions(query:)
62
+ return { addresses: [] } if blank?(query)
63
+
64
+ session_token = SecureRandom.uuid
65
+ params = URI.encode_www_form(query: query, sessionToken: session_token)
66
+ url = "#{@address_endpoint}/_address-input/autocomplete?#{params}"
67
+ log("REST GET -> #{@address_endpoint}/_address-input/autocomplete")
68
+
69
+ response = fetch_with_timeout(url, method: :get, headers: { "Content-Type" => "application/json" })
70
+ ensure_ok!(response, "Autocomplete request failed", "NETWORK_ERROR")
71
+
72
+ raw = parse_json_response(response.body, http_status: response.code.to_i)
73
+ predictions = raw.fetch("predictions", [])
74
+ addresses = predictions.map do |prediction|
75
+ begin
76
+ resolve_prediction(prediction, session_token)
77
+ rescue StandardError
78
+ nil
79
+ end
80
+ end.compact
81
+
82
+ { addresses: addresses }
83
+ end
84
+
85
+ def create_offer(data:)
86
+ Validators.validate_create_offer_request(data)
87
+ address = get(data, :address)
88
+
89
+ result = graphql(
90
+ query: Graphql::OFFER_REQUEST_CREATE,
91
+ variables: {
92
+ offerRequestCreateInput: {
93
+ address: normalize_address(address),
94
+ saleAssociateEmail: get(data, :sales_associate_email, :salesAssociateEmail),
95
+ saleAssociateName: get(data, :sales_associate_name, :salesAssociateName)
96
+ }.compact
97
+ }
98
+ )
99
+
100
+ symbolize_keys(result.fetch("offerRequestCreate").fetch("offerRequest"))
101
+ end
102
+
103
+ def update_offer(offer_id:, data:)
104
+ Validators.validate_update_offer_request(offer_id, data)
105
+ transformed = AnswerKeyMapping.transform_answers_to_offer_input(stringify_keys(data))
106
+ offer_input = {}
107
+
108
+ transformed.each do |key, value|
109
+ next if value.nil?
110
+ next unless VALID_OFFER_INPUT_FIELDS.include?(key)
111
+
112
+ offer_input[key] = if value.is_a?(String) && ENUM_FIELDS.include?(key)
113
+ value.upcase
114
+ else
115
+ value
116
+ end
117
+ end
118
+
119
+ result = graphql(
120
+ query: Graphql::OFFER_REQUEST_UPDATE,
121
+ variables: {
122
+ offerRequestUpdateInput: {
123
+ opendoorOfferRequestId: offer_id,
124
+ offerInput: offer_input
125
+ }
126
+ }
127
+ )
128
+
129
+ symbolize_keys(result.fetch("offerRequestUpdate").fetch("offerRequest"))
130
+ end
131
+
132
+ def get_offer(offer_id:)
133
+ raise ValidationError.new("offerId is required", "offerId") if blank?(offer_id)
134
+
135
+ result = graphql(
136
+ query: Graphql::OFFER_REQUEST_QUERY,
137
+ variables: { opendoorOfferRequestId: offer_id }
138
+ )
139
+ symbolize_keys(result.fetch("offerRequest"))
140
+ end
141
+
142
+ def get_offer_with_sell_direct_pricing(offer_id:)
143
+ raise ValidationError.new("offerId is required", "offerId") if blank?(offer_id)
144
+
145
+ result = graphql(
146
+ query: Graphql::OFFER_REQUEST_WITH_SELL_DIRECT_PRICING,
147
+ variables: { opendoorOfferRequestId: offer_id }
148
+ )
149
+ symbolize_keys(result.fetch("offerRequestWithSellDirectPricing"))
150
+ end
151
+
152
+ def check_address_unit(address:)
153
+ raise ValidationError.new("street1 is required", "street1") if blank?(get(address, :street1))
154
+
155
+ result = graphql(
156
+ query: Graphql::ADDRESS_UNIT_CHECK,
157
+ variables: { address: normalize_address(address) }
158
+ )
159
+ symbolize_keys(result.fetch("addressUnitCheck"))
160
+ end
161
+
162
+ def get_address_details(address:, tracking_id: nil)
163
+ raise ValidationError.new("street1 is required", "street1") if blank?(get(address, :street1))
164
+
165
+ variables = { address: normalize_address(address) }
166
+ variables[:trackingId] = tracking_id if tracking_id
167
+
168
+ result = graphql(query: Graphql::ADDRESS_DETAILS_QUERY, variables: variables)
169
+ symbolize_keys(result.fetch("address"))
170
+ end
171
+
172
+ def get_homebuilders
173
+ log("GraphQL (gateway) -> #{@gateway_endpoint}")
174
+ response = fetch_with_timeout(
175
+ @gateway_endpoint,
176
+ method: :post,
177
+ headers: {
178
+ "Content-Type" => "application/json",
179
+ "apollographql-client-name" => "partner-sdk",
180
+ "apollographql-client-version" => "1.0"
181
+ },
182
+ body: JSON.generate(
183
+ query: Graphql::LIST_PARTNERS_QUERY,
184
+ variables: { partnershipType: "HOME_BUILDER" }
185
+ )
186
+ )
187
+
188
+ ensure_ok!(response, "Homebuilders request failed", "GRAPHQL_ERROR")
189
+ json = parse_json_response(response.body, http_status: response.code.to_i)
190
+ ensure_graphql_data!(json)
191
+
192
+ partners = json.dig("data", "partnership", "listPartners", "partners")
193
+ unless partners.is_a?(Array)
194
+ raise ApiError.new("GraphQL response missing partnership.listPartners.partners", "GRAPHQL_ERROR", 200)
195
+ end
196
+
197
+ {
198
+ homebuilders: partners.map do |partner|
199
+ {
200
+ identifier: partner["identifier"],
201
+ displayName: partner["displayName"]
202
+ }
203
+ end
204
+ }
205
+ end
206
+
207
+ def get_assessment_slots(offer_id:)
208
+ raise ValidationError.new("offerId is required", "offerId") if blank?(offer_id)
209
+
210
+ api_base = graphql_api_origin
211
+ url = "#{api_base}/v2/exterior_assessment_slots"
212
+ log("REST POST -> #{url}")
213
+
214
+ response = fetch_with_timeout(
215
+ url,
216
+ method: :post,
217
+ headers: {
218
+ "Content-Type" => "application/json",
219
+ "Authorization" => "Basic #{@api_key}"
220
+ },
221
+ body: JSON.generate(
222
+ partnerId: @api_key,
223
+ sellerInputUuid: offer_id,
224
+ calendarType: "DILIGENCE_VISIT"
225
+ )
226
+ )
227
+ ensure_ok!(response, "Assessment slots request failed", "NETWORK_ERROR")
228
+ raw = parse_json_response(response.body, http_status: response.code.to_i)
229
+
230
+ unless raw["success"]
231
+ raise ApiError.new(raw["error"] || "Assessment slots request failed", "API_ERROR", 200)
232
+ end
233
+
234
+ {
235
+ currentInspectionTime: raw["currentInspectionTime"],
236
+ availableSlots: raw["availableSlots"] || [],
237
+ isSlotBased: raw["isSlotBased"] || false,
238
+ visitId: raw["visitId"] || "",
239
+ timezone: raw["timezone"] || "UTC"
240
+ }
241
+ end
242
+
243
+ def get_home_detail(opendoor_offer_request_id:)
244
+ raise ValidationError.new("opendoorOfferRequestId is required", "opendoorOfferRequestId") if blank?(opendoor_offer_request_id)
245
+
246
+ data = graphql(
247
+ query: Graphql::SELLER_INPUT_PREFILLS_QUERY,
248
+ variables: { sellerInputUuid: opendoor_offer_request_id }
249
+ )
250
+
251
+ seller_prefills = data["sellerInputPrefills"]
252
+ raise ApiError.new("Home detail not found", "NOT_FOUND", 404) if seller_prefills.nil?
253
+
254
+ { answerPrefills: seller_prefills["answerPrefills"] || {} }
255
+ end
256
+
257
+ private
258
+
259
+ def resolve_prediction(prediction, session_token)
260
+ if prediction["street"] && prediction["city"] && prediction["state"] && prediction["postalCode"]
261
+ return {
262
+ street1: prediction["street"],
263
+ street2: prediction["unitInfo"],
264
+ city: prediction["city"],
265
+ state: prediction["state"],
266
+ postalCode: prediction["postalCode"]
267
+ }.compact
268
+ end
269
+
270
+ place_id = prediction["place_id"]
271
+ raise "Cannot resolve prediction" if blank?(place_id)
272
+
273
+ get_place_details(place_id, session_token)
274
+ end
275
+
276
+ def get_place_details(place_id, session_token)
277
+ params = URI.encode_www_form(placeId: place_id, sessionToken: session_token)
278
+ url = "#{@address_endpoint}/_address-input/place?#{params}"
279
+ response = fetch_with_timeout(url, method: :get, headers: { "Content-Type" => "application/json" })
280
+ ensure_ok!(response, "Place details request failed", "NETWORK_ERROR")
281
+ raw = parse_json_response(response.body, http_status: response.code.to_i)
282
+
283
+ {
284
+ street1: raw["street1"].to_s.strip,
285
+ street2: raw["street2"],
286
+ city: raw["city"],
287
+ state: raw["state"],
288
+ postalCode: raw["postal_code"]
289
+ }.compact
290
+ end
291
+
292
+ def graphql(query:, variables:)
293
+ response = fetch_with_timeout(
294
+ @api_endpoint,
295
+ method: :post,
296
+ headers: {
297
+ "Content-Type" => "application/json",
298
+ "Authorization" => "Basic #{@api_key}"
299
+ },
300
+ body: JSON.generate(query: query, variables: variables)
301
+ )
302
+ ensure_ok!(response, "GraphQL request failed", "GRAPHQL_ERROR")
303
+
304
+ json = parse_json_response(response.body, http_status: response.code.to_i)
305
+ ensure_graphql_data!(json)
306
+ json.fetch("data")
307
+ end
308
+
309
+ def ensure_graphql_data!(json)
310
+ if json["errors"].is_a?(Array) && !json["errors"].empty?
311
+ message = json["errors"].map { |error| error["message"] }.join(", ")
312
+ raise ApiError.new("GraphQL errors: #{message}", "GRAPHQL_ERROR", 200, json["errors"])
313
+ end
314
+
315
+ raise ApiError.new("GraphQL response missing data", "GRAPHQL_ERROR", 200) if json["data"].nil?
316
+ end
317
+
318
+ def ensure_ok!(response, prefix, code)
319
+ status = response.code.to_i
320
+ return if status >= 200 && status < 300
321
+
322
+ raise ApiError.new("#{prefix}: #{status} #{response.message}", code, status)
323
+ end
324
+
325
+ def fetch_with_timeout(url, method:, headers:, body: nil)
326
+ uri = URI.parse(url)
327
+ request_class = method == :post ? Net::HTTP::Post : Net::HTTP::Get
328
+ request = request_class.new(uri.request_uri)
329
+ headers.each { |k, v| request[k] = v }
330
+ request.body = body if body
331
+
332
+ http = Net::HTTP.new(uri.host, uri.port)
333
+ http.use_ssl = uri.scheme == "https"
334
+ http.open_timeout = @timeout_seconds
335
+ http.read_timeout = @timeout_seconds
336
+ http.request(request)
337
+ rescue Net::OpenTimeout, Net::ReadTimeout
338
+ raise ApiError.new("Request timed out after #{@timeout_ms}ms", "TIMEOUT", 0)
339
+ rescue SocketError, Errno::ECONNREFUSED, IOError => e
340
+ raise ApiError.new("Network error: #{e.message}", "NETWORK_ERROR", 0)
341
+ end
342
+
343
+ def normalize_address(address)
344
+ {
345
+ street1: get(address, :street1),
346
+ street2: get(address, :street2),
347
+ city: get(address, :city),
348
+ state: get(address, :state),
349
+ postalCode: get(address, :postal_code, :postalCode)
350
+ }.compact
351
+ end
352
+
353
+ def stringify_keys(value)
354
+ if value.is_a?(Hash)
355
+ value.each_with_object({}) do |(k, v), h|
356
+ h[k.to_s] = stringify_keys(v)
357
+ end
358
+ elsif value.is_a?(Array)
359
+ value.map { |v| stringify_keys(v) }
360
+ else
361
+ value
362
+ end
363
+ end
364
+
365
+ def symbolize_keys(value)
366
+ if value.is_a?(Hash)
367
+ value.each_with_object({}) do |(k, v), h|
368
+ h[k.to_sym] = symbolize_keys(v)
369
+ end
370
+ elsif value.is_a?(Array)
371
+ value.map { |v| symbolize_keys(v) }
372
+ else
373
+ value
374
+ end
375
+ end
376
+
377
+ def get(hash, *keys)
378
+ return nil unless hash.is_a?(Hash)
379
+
380
+ keys.each do |key|
381
+ return hash[key] if hash.key?(key)
382
+ return hash[key.to_s] if hash.key?(key.to_s)
383
+ end
384
+ nil
385
+ end
386
+
387
+ def blank?(value)
388
+ value.nil? || value.to_s.strip.empty?
389
+ end
390
+
391
+ # Same-origin base as JS `new URL(apiEndpoint).origin` (includes non-default port).
392
+ def graphql_api_origin
393
+ uri = URI.parse(@api_endpoint)
394
+ if uri.respond_to?(:origin) && !uri.origin.nil?
395
+ uri.origin
396
+ else
397
+ "#{uri.scheme}://#{uri.host}#{uri.port && ![80, 443].include?(uri.port) ? ":#{uri.port}" : ""}"
398
+ end
399
+ end
400
+
401
+ def parse_json_response(body, http_status:)
402
+ JSON.parse(body.to_s)
403
+ rescue JSON::ParserError => e
404
+ raise ApiError.new("Invalid JSON response: #{e.message}", "PARSE_ERROR", http_status)
405
+ end
406
+
407
+ def log(message)
408
+ puts("[OpendoorClient] #{message}") if @debug
409
+ end
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,24 @@
1
+ module Opendoor
2
+ module PartnerSdk
3
+ class ValidationError < StandardError
4
+ attr_reader :code, :field
5
+
6
+ def initialize(message, field = nil)
7
+ super(message)
8
+ @code = "VALIDATION_ERROR"
9
+ @field = field
10
+ end
11
+ end
12
+
13
+ class ApiError < StandardError
14
+ attr_reader :code, :status_code, :graphql_errors
15
+
16
+ def initialize(message, code, status_code, graphql_errors = nil)
17
+ super(message)
18
+ @code = code
19
+ @status_code = status_code
20
+ @graphql_errors = graphql_errors
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module Opendoor
2
+ module PartnerSdk
3
+ module Graphql
4
+ OFFER_REQUEST_CREATE = <<~GRAPHQL.freeze
5
+ mutation OfferRequestCreate($offerRequestCreateInput: OfferRequestCreateInput!) {
6
+ offerRequestCreate(params: $offerRequestCreateInput) {
7
+ offerRequest {
8
+ opendoorOfferRequestId
9
+ offerStatus
10
+ denialInfo {
11
+ deniedAt
12
+ code
13
+ explanation
14
+ }
15
+ }
16
+ }
17
+ }
18
+ GRAPHQL
19
+
20
+ OFFER_REQUEST_UPDATE = <<~GRAPHQL.freeze
21
+ mutation OfferRequestUpdate($offerRequestUpdateInput: OfferRequestUpdateInput!) {
22
+ offerRequestUpdate(params: $offerRequestUpdateInput) {
23
+ offerRequest {
24
+ opendoorOfferRequestId
25
+ offerStatus
26
+ denialInfo {
27
+ deniedAt
28
+ code
29
+ explanation
30
+ }
31
+ }
32
+ }
33
+ }
34
+ GRAPHQL
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,120 @@
1
+ module Opendoor
2
+ module PartnerSdk
3
+ module Graphql
4
+ OFFER_REQUEST_QUERY = <<~GRAPHQL.freeze
5
+ query OfferRequest($opendoorOfferRequestId: String!) {
6
+ offerRequest(opendoorOfferRequestId: $opendoorOfferRequestId) {
7
+ opendoorOfferRequestId
8
+ offerStatus
9
+ denialInfo {
10
+ deniedAt
11
+ code
12
+ explanation
13
+ }
14
+ offerData {
15
+ headlinePriceCents
16
+ headlineValuationRangeStart
17
+ headlineValuationRangeEnd
18
+ feeCostCents
19
+ feeCostPercent
20
+ estimatedClosingCostCents
21
+ estimatedClosingCostPercent
22
+ repairCostCents
23
+ url
24
+ authenticatedDashboardUrl
25
+ offerSource
26
+ sentAt
27
+ expirationAt
28
+ expirationAtDisplayDate
29
+ underwritingCompletedAt
30
+ underwritingState
31
+ enteredContractAt
32
+ coeDate
33
+ closedAt
34
+ withdrawnAt
35
+ priceAlert
36
+ }
37
+ handoverAt
38
+ }
39
+ }
40
+ GRAPHQL
41
+
42
+ OFFER_REQUEST_WITH_SELL_DIRECT_PRICING = <<~GRAPHQL.freeze
43
+ query OfferRequestWithSellDirectPricing($opendoorOfferRequestId: String!) {
44
+ offerRequestWithSellDirectPricing(opendoorOfferRequestId: $opendoorOfferRequestId) {
45
+ opendoorOfferRequestId
46
+ offerStatus
47
+ denialInfo {
48
+ deniedAt
49
+ code
50
+ explanation
51
+ }
52
+ offerData {
53
+ netProceeds
54
+ sentAt
55
+ expirationAt
56
+ expirationAtDisplayDate
57
+ mortgageBalance
58
+ sellerName
59
+ sellerEmail
60
+ headlinePriceCents
61
+ opendoorFeeCents
62
+ repairCostCents
63
+ closingCostsCents
64
+ }
65
+ }
66
+ }
67
+ GRAPHQL
68
+
69
+ ADDRESS_UNIT_CHECK = <<~GRAPHQL.freeze
70
+ query AddressUnitCheck($address: AddressInput!) {
71
+ addressUnitCheck(address: $address) {
72
+ requiresUnitNumber
73
+ }
74
+ }
75
+ GRAPHQL
76
+
77
+ LIST_PARTNERS_QUERY = <<~GRAPHQL.freeze
78
+ query ListPartnersQuery($partnershipType: OdProtosPartnershipData_PartnershipType!) {
79
+ partnership {
80
+ listPartners(input: { partnershipType: $partnershipType }) {
81
+ partners {
82
+ identifier
83
+ displayName
84
+ }
85
+ }
86
+ }
87
+ }
88
+ GRAPHQL
89
+
90
+ ADDRESS_DETAILS_QUERY = <<~GRAPHQL.freeze
91
+ query Address($address: AddressInput!, $trackingId: String) {
92
+ address(address: $address) {
93
+ products {
94
+ homeSell {
95
+ isEligible
96
+ denialCode
97
+ denialExplanation
98
+ valueEstimate {
99
+ lower
100
+ upper
101
+ }
102
+ referral(trackingId: $trackingId) {
103
+ url
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ GRAPHQL
110
+
111
+ SELLER_INPUT_PREFILLS_QUERY = <<~GRAPHQL.freeze
112
+ query SellerInputPrefills($sellerInputUuid: String!) {
113
+ sellerInputPrefills(sellerInputUuid: $sellerInputUuid) {
114
+ answerPrefills
115
+ }
116
+ }
117
+ GRAPHQL
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,55 @@
1
+ module Opendoor
2
+ module PartnerSdk
3
+ module Validators
4
+ module_function
5
+
6
+ def validate_address(address)
7
+ raise ValidationError.new("street1 is required", "address.street1") if blank?(get(address, :street1))
8
+ raise ValidationError.new("city is required", "address.city") if blank?(get(address, :city))
9
+ raise ValidationError.new("state is required", "address.state") if blank?(get(address, :state))
10
+ raise ValidationError.new("postalCode is required", "address.postalCode") if blank?(get(address, :postal_code, :postalCode))
11
+ end
12
+
13
+ def validate_create_offer_request(data)
14
+ unless data.is_a?(Hash)
15
+ raise ValidationError.new("create data must be a Hash", "data")
16
+ end
17
+
18
+ address = get(data, :address)
19
+ raise ValidationError.new("address is required", "address") if address.nil?
20
+ raise ValidationError.new("address must be a Hash", "address") unless address.is_a?(Hash)
21
+
22
+ validate_address(address)
23
+ end
24
+
25
+ def validate_update_offer_request(offer_id, data)
26
+ raise ValidationError.new("offerId is required", "offerId") if blank?(offer_id)
27
+ unless data.is_a?(Hash)
28
+ raise ValidationError.new("update data must be a Hash", "data")
29
+ end
30
+ raise ValidationError.new(
31
+ "At least one OfferInput field must be provided",
32
+ "offerInput"
33
+ ) if data.empty?
34
+ end
35
+
36
+ def blank?(value)
37
+ value.nil? || value.to_s.strip.empty?
38
+ end
39
+
40
+ def get(hash, *keys)
41
+ return nil unless hash.is_a?(Hash)
42
+
43
+ keys.each do |key|
44
+ [key, key.to_s].uniq.each do |k|
45
+ next unless hash.key?(k)
46
+
47
+ v = hash[k]
48
+ return v unless v.nil?
49
+ end
50
+ end
51
+ nil
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ module Opendoor
2
+ module PartnerSdk
3
+ VERSION = "1.1.6.beta.89.1"
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "partner_sdk/version"
2
+ require_relative "partner_sdk/errors"
3
+ require_relative "partner_sdk/validators"
4
+ require_relative "partner_sdk/answer_key_mapping"
5
+ require_relative "partner_sdk/graphql/queries"
6
+ require_relative "partner_sdk/graphql/mutations"
7
+ require_relative "partner_sdk/client"
8
+
9
+ module Opendoor
10
+ module PartnerSdk
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opendoor-partner-sdk-server-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.6.beta.89.1
5
+ platform: ruby
6
+ authors:
7
+ - Opendoor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.22'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.22'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.2'
41
+ description: Ruby SDK for partner backend integrations with Opendoor APIs. Distributed
42
+ as a packaged gem only; see LICENSE.txt. Source maintained by Opendoor Labs is proprietary.
43
+ email:
44
+ - partnerships@opendoor.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE.txt
50
+ - README.md
51
+ - lib/opendoor/partner_sdk.rb
52
+ - lib/opendoor/partner_sdk/answer_key_mapping.rb
53
+ - lib/opendoor/partner_sdk/client.rb
54
+ - lib/opendoor/partner_sdk/errors.rb
55
+ - lib/opendoor/partner_sdk/graphql/mutations.rb
56
+ - lib/opendoor/partner_sdk/graphql/queries.rb
57
+ - lib/opendoor/partner_sdk/validators.rb
58
+ - lib/opendoor/partner_sdk/version.rb
59
+ homepage: https://partner-sdk.opendoor.com/
60
+ licenses:
61
+ - NONE
62
+ metadata:
63
+ homepage_uri: https://partner-sdk.opendoor.com/
64
+ documentation_uri: https://partner-sdk.opendoor.com/
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.7.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.5.22
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Server-side Ruby SDK for Opendoor partner integrations
84
+ test_files: []