Skip to content

DTC Onboarding Flow

The DtcOnboardingFlow component renders the traditional (non-instant-offer) questionnaire, matching the DTC flow on opendoor.com. It presents 17 pages of questions — one per screen — with a two-column layout on desktop and stacked layout on mobile. It handles navigation, validation, conditional page skipping, and fires onSubmit when the user completes the flow.

DtcOnboardingFlow component

Basic usage

import { useState } from 'react';
import {
DtcOnboardingFlow,
OpendoorProvider,
} from '@opendoor/partner-sdk-client-react';
import { OpendoorClient } from '@opendoor/partner-sdk-client-js-core';
import type { Address } from '@opendoor/partner-sdk-client-js-core';
const client = new OpendoorClient({ baseURL: '/api/opendoor/v1' });
function App({ address }: { address: Address }) {
const [offerId, setOfferId] = useState<string | null>(null);
// Step 1: Create the offer request BEFORE showing the questionnaire
const startFlow = async () => {
const offer = await client.createOffer({ address });
if (offer.offerStatus === 'DENIED') {
console.log('Denied:', offer.denialInfo?.explanation);
return;
}
setOfferId(offer.opendoorOfferRequestId);
};
if (!offerId) {
return <button onClick={startFlow}>Get an Offer</button>;
}
// Step 2: Submit answers via updateOffer when the questionnaire completes
const handleSubmit = async (answers: Record<string, unknown>) => {
await client.updateOffer(offerId, answers);
// Step 3: Poll client.getOffer(offerId) until status is OFFERED or DENIED
};
return (
<OpendoorProvider client={client}>
<DtcOnboardingFlow
address={address}
appearance={{ theme: 'minimal' }}
partnerLogoUrl="/your-logo.svg"
onSubmit={handleSubmit}
onClose={() => console.log('User closed the flow')}
/>
</OpendoorProvider>
);
}

Wrap the component in <OpendoorProvider> to supply the SDK client.

Required BFF endpoints

RouteBacking server SDK call
POST /api/opendoor/v1/offer/createopendoor.createOffer(...)
POST /api/opendoor/v1/offer/updateopendoor.updateOffer(offerId, ...)
POST /api/opendoor/v1/offeropendoor.getOffer(offerId)
POST /api/opendoor/v1/homebuildersopendoor.getHomebuilders()

See Quick Start step 1 for complete Node (Express) and Ruby (Rails) backend examples.

Props

PropTypeDefaultDescription
addressAddressRequiredThe selected property address
onSubmit(answers) => voidRequiredCalled when user completes all pages
partnerLogoUrlstring-Partner logo URL (shown before the Opendoor logo in header)
appearanceOpendoorAppearance{ theme: 'minimal' }Visual theme configuration
initialAnswersRecord<string, AnswerValue>-Prefill data (e.g., from address lookup)
marketstring-Market identifier for region-specific questions
smsConsentTextReactNode-Custom SMS opt-in toggle label on the contact page (below)
contactConsentTextReactNode-Custom legal disclaimer below the submit button on the contact page (below)
onClose() => void-Called when close (X) button is clicked

Callbacks & Events

CallbackTypeWhen it fires
onReady() => voidComponent mounted and ready
onSubmit(answers) => voidUser completed all pages
onPageChange(pageIndex, pageId) => voidUser navigates between pages
onAnswerChange(key, value) => voidAny answer changes
onError(error: Error) => voidValidation or internal error
onClose() => voidClose button clicked

Pages

The component renders 17 pages. Pages with conditional visibility are automatically skipped when their conditions are not met (e.g., HOA sub-pages are skipped when HOA = No).

#PageWhat it collects
1Confirm Home DetailsDwelling type, beds, baths, sqft, stories, basement, year, pool
2OwnershipRelationship to owner (owner, agent, other)
3Sale TimelineWhen the user needs to sell
4-7Room Condition (×4)Kitchen, bathroom, living room, exterior — 5-level image select
8HOAIs the home part of an HOA?
9HOA TypeAge-restricted, gated community (if HOA = Yes)
10HOA GuardGuard at entrance (if gated community)
11HOA FeesMonthly fees (if HOA = Yes, optional)
12Eligibility CriteriaDisqualifying property features (solar, foundation, etc.)
13UpgradesHas the home had upgrades?
14HomebuilderWorking with a homebuilder?
15Homebuilder NameWhich homebuilder (if yes)
16Homebuilder DetailsSales associate email + community name (if yes)
17Contact InfoName, email, phone, SMS consent

The contact page (page 17) has two independently configurable consent strings:

  • smsConsentText / #sms-consent-text — the SMS marketing opt-in toggle label. Defaults to Opendoor’s standard SMS consent copy. Override to add your company name for co-consent. In React, pass a ReactNode prop; in Vue, use the #sms-consent-text slot.
  • contactConsentText / #contact-consent-text — the legal disclaimer rendered below the “See my offer” button. Defaults to Opendoor’s Terms of Service and Privacy Policy disclaimer. In React, pass a ReactNode prop; in Vue, use the #contact-consent-text slot.

Customizing the SMS opt-in label

<DtcOnboardingFlow
address={address}
smsConsentText="I consent to receive marketing calls and texts from Opendoor and its affiliates to the number provided. I also agree to be contacted by Acme Co. I understand that I may opt out at any time and consent is not a condition of purchase."
onSubmit={handleSubmit}
/>
<DtcOnboardingFlow
address={address}
contactConsentText={
<p>
By clicking "See my offer," I agree to Acme Co.’s{‘ ‘}
<a href="https://acme.co/terms">Terms of Service</a> and{‘ ‘}
<a href="https://acme.co/privacy">Privacy Policy</a>.
</p>
}
onSubmit={handleSubmit}
/>

Prefilling answers

Pass initialAnswers to pre-populate fields (e.g., from property data). For the full list of keys, config types, and allowed option values, see the field reference below.

<DtcOnboardingFlow
address={address}
initialAnswers={{
'home.dwelling_type': 'single_family',
'home.bedrooms': 3,
'home.bathrooms.full': 2,
'home.above_grade_sq_ft': 1800,
'home.year_built': 2010,
}}
onSubmit={handleSubmit}
/>

Loading prefills from getHomeDetail

Call client.getHomeDetail({ opendoorOfferRequestId }) after createOffer to fetch prefills from MLS and public records. Pass the result as initialAnswers. See the field reference below for available keys.

async function startFlow(address: Address) {
const offer = await client.createOffer({ address });
const { answerPrefills } = await client.getHomeDetail({
opendoorOfferRequestId: offer.opendoorOfferRequestId,
});
const initialAnswers = Object.fromEntries(
Object.entries(
answerPrefills as Record<string, { value: unknown }>
).map(([key, entry]) => [key, entry.value])
);
// Render with the loaded prefills:
// <OpendoorProvider client={client}>
// <DtcOnboardingFlow
// address={address}
// initialAnswers={initialAnswers}
// onSubmit={(answers) => client.updateOffer(offer.opendoorOfferRequestId, answers)}
// />
// </OpendoorProvider>
}

Field reference

Use these dot-notation keys with initialAnswers on DtcOnboardingFlow (React) or :initial-answers (Vue). Shapes match what you pass to updateOffer. Aggregate type for values is AnswerValue: string | number | boolean | string[] | undefined (from @opendoor/partner-sdk-client-js-core/internal/questionnaire).

PageKeyConfig typeRequiredAllowed values / notes
confirm-home-detailshome.dwelling_typestringyessingle_family, townhouse, apartment, mobile_home
confirm-home-detailshome.bedroomsnumberyes1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
confirm-home-detailshome.bathrooms.fullnumberyes1, 2, 3, 4, 5, 6, 7, 8, 9, 10
confirm-home-detailshome.bathrooms.halfnumberno0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
confirm-home-detailshome.exterior_storiesnumberyes1, 2, 3
confirm-home-detailshome.above_grade_sq_ftnumberyes(no fixed enum)
confirm-home-detailshome.has_basementstringyesyes, no
confirm-home-detailshome.basement_finished_sq_ftnumberno(no fixed enum)
confirm-home-detailshome.basement_unfinished_sq_ftnumberno(no fixed enum)
confirm-home-detailshome.year_builtnumberyes18772026 (150 values)
confirm-home-detailshome.pool_typestringyesin_ground, above_ground, community_pool, no_pool
confirm-home-detailshome.covered_parking_typestringnogarage, carport, none
confirm-home-detailshome.garage_spacesnumberno0, 1, 2, 3, 4, 5
confirm-home-detailshome.entry_typestringyesdirect_entry, shared_entrance_condo
ownershipseller.relation_to_ownerstringyesself, agent
sale-timelineseller.sale_timelinestringyesASAP, 2_TO_4_WEEKS, 4_TO_6_WEEKS, 6_PLUS_WEEKS, JUST_BROWSING
kitchen-conditionhome.kitchen_seller_scorestringyesfixer_upper, dated, standard, high_end, luxury
bathroom-conditionhome.bathroom_seller_scorestringyesfixer_upper, dated, standard, high_end, luxury
living-room-conditionhome.living_room_seller_scorestringyesfixer_upper, dated, standard, high_end, luxury
exterior-conditionhome.exterior_seller_scorestringyesfixer_upper, dated, standard, high_end, luxury
hoahome.hoastringyesyes, no
hoa-typehome.hoa_typestring[]yesage_restricted_community, gated_community, none
hoa-guardhome.hoa_type.guarded_gated_communitystringyeshas_guard, no_guard
hoa-feeshome.hoa_feesnumberno(no fixed enum)
eligibility-criteriahome.eligibility_criteriastring[]noleased_solar_panels, known_foundation_issues, fire_damage, well_water, septic, unique_ownership_structure, below_market_rate_ownership, rent_controlled_tenant_occupied, mobile_manufactured_home, none
upgradeshome.has_upgradesstringyesyes, no
homebuilderseller.working_with_home_builderstringyestrue, false
homebuilder-nameseller.home_builderstringno(no fixed enum)
homebuilder-detailsseller.home_builder_emailstringno(no fixed enum)
homebuilder-detailsseller.home_builder_communitystringno(no fixed enum)
contact-infoseller.full_namestringyes(no fixed enum)
contact-infoseller.emailstringyes(no fixed enum)
contact-infoseller.phone_numberstringyes(no fixed enum)
contact-infoseller.sms_opt_inbooleanno

Styling

The component uses the appearance prop for theming. It defaults to the minimal theme. All standard tokens apply — colors, fonts, spacing, borders, and radii are shared with other SDK components like AddressEntry and QualificationQuestions:

<DtcOnboardingFlow
address={address}
appearance={{
theme: 'minimal',
variables: {
colorPrimary: '#003366',
fontFamily: '"Inter", sans-serif',
colorCardBorderSelected: '#003366',
colorCardBackgroundSelected: '#f0f5ff',
},
}}
onSubmit={handleSubmit}
/>

The card selection tokens (colorCardBackground, colorCardBorder, colorCardBorderSelected, colorCardBackgroundSelected) control the selectable card components used for radio and checkbox inputs.

See the Styling & Themes guide for all available tokens.

Resetting the form

To reset the component (e.g., start over with a different address), change the key prop:

<DtcOnboardingFlow
key={address.street1}
address={address}
onSubmit={handleSubmit}
/>