Lifecycle of an offer request
The partner GraphQL API exposes three mutations that drive an offer request: offerRequestCreate, offerRequestUpdate, and (later) scheduleAssessment. The naming is a little misleading — offerRequestCreate doesn’t create an offer, it creates an offer request (a placeholder bucket that becomes a fully-priced offer once enough data is in). This page walks through the state machine end-to-end so partners building against the API know what to expect at each step.
State machine
flowchart TD
Start([ Partner calls offerRequestCreate(address) ]) --> Create
Create["Reception creates SellerInput<br/>(no Customer, no Lead, no Offer yet)"]
Create -->|Address-collision or<br/>region/buybox denial| DeniedAtCreate[DENIED]
Create -->|Otherwise| Pending1[PENDING]
Pending1 --> Update1["Partner calls offerRequestUpdate<br/>with home-detail answers"]
Update1 -->|Disqualifying answer<br/>e.g. solar lien, foundation| DeniedAtUpdate[DENIED]
Update1 -->|Otherwise| Pending2[PENDING]
Pending2 --> Update2["Partner calls offerRequestUpdate<br/>with contact info<br/>(name, email, phone)"]
Update2 --> Qualified["Lead qualified<br/>Customer created/linked<br/>OfferProcessJob runs pricing"]
Qualified -->|Valuation/pricing fails| DeniedAtPricing["DENIED<br/>(UNABLE_TO_VALUE,<br/>VALUATION_TOO_HIGH/LOW, etc.)"]
Qualified -->|Otherwise| Offered[OFFERED]
DeniedAtCreate -.->|terminal| End1([Stop the flow])
DeniedAtUpdate -.->|terminal| End2([Stop the flow])
DeniedAtPricing -.->|terminal| End3([Stop the flow])
Offered --> Schedule[/Optional: scheduleAssessment/]
Step-by-step
1. offerRequestCreate(address)
Creates a SellerInput keyed by address + a generated UUID. No Customer record is created at this step — the partner hasn’t sent any seller identity yet. The response includes the opendoorOfferRequestId you’ll use for subsequent calls.
Denials that can fire here: address-collision codes (ACTIVE_OFFER, IN_CONTRACT_WITH_OPENDOOR, LISTED_BY_OPENDOOR, OWNED_BY_OPENDOOR, PREPARING_OFFER) and region/buybox codes derivable from the address alone (ADDRESS, PRODUCT_INACTIVE, parcel-derivable codes like DWELLING_TYPE, LOT_SQ_FT, YEAR_BUILT, FLOOD_ZONE). See Denial codes reference for the full list.
Always check denialInfo on the create response and treat any non-null value as terminal. Address-collision codes specifically don’t get re-evaluated on later updates.
2. offerRequestUpdate with home-detail answers
Partner submits questionnaire answers (homeDwellingType, homeBedrooms, homeEligibilityCriteriaLeasedSolarPanels, etc.) in one or more offerRequestUpdate calls. The mutation is idempotent — incremental and batched calls both work, and the BFF runs the same eligibility pipeline on every call regardless of how many answers you submit at once.
Denials can fire here as soon as a disqualifying answer lands. If the seller answers “yes” to leased solar panels or known foundation issues, denialInfo returns on that same response — no contact info required. Surface it and stop the flow.
3. offerRequestUpdate with contact info
Once the partner submits sellerFullName, sellerEmail, and sellerPhoneNumber, the Lead is qualified: a Customer record is created/linked, and the offer process kicks off the pricing pipeline (comps, valuation, NetCash, etc.).
Contact info is required for an OFFERED status with a price. Denials don’t need contact info, but offers do.
4. Final state
After pricing runs, the offer either lands in OFFERED (with offerData, netProceeds, etc.) or returns a pricing-pipeline denial like VALUATION_TOO_HIGH / VALUATION_TOO_LOW / OTHER_DENIAL_DETAIL (often surfacing UNABLE_TO_VALUE from the AVM).
From here, the partner can optionally call scheduleAssessment to book the in-home visit.
Gotchas
”Offer request” ≠ “offer”
The naming is misleading even for internal eng. offerRequestCreate does not create an offer. It creates the empty SellerInput bucket that becomes an offer once enough data + contact info lands and the pricing pipeline finishes. Mentally treat offerRequest as “the conversation about a potential offer” and offer as “the priced terms we ended up at.”
Denials are terminal
Both internally on opendoor.com and on the partner API, denials don’t get “fixed” mid-flow. Once offerStatus !== PENDING, the BFF rejects home-detail updates — if you try to offerRequestUpdate with homeEligibilityCriteriaLeasedSolarPanels: false after a denial, you get a ForbiddenError. Contact-info fields (sellerEmail, sellerPhone, etc.) are still updatable; home-detail fields are locked.
Practical implication: surface the denial in your UX and stop the flow. Don’t design a “let the user correct their answer and try again” loop — it won’t work against this API.
Incremental vs batched updates
Both calling patterns work; there’s no server-side preference. The trade-off is purely UX:
| Pattern | Pros | Cons |
|---|---|---|
| Incremental (per step) | Short-circuit the flow as soon as a denial fires. Save the seller time on subsequent questions they won’t need. | More network calls. Slightly chattier integration. |
| Batched (final submit) | One round-trip per offer request. Simpler client. | Seller fills out the entire questionnaire even when an early answer would have denied them. |
Most partner integrations go incremental — the UX benefit of catching denials early outweighs the chatter.
Contact info gates pricing, not denials
A nuance worth restating: you can get a DENIED response before contact info is ever submitted. What you can’t get pre-contact-info is an OFFERED response with a price — the pricing pipeline requires the Customer to exist, and the Customer is created at lead qualification (which needs name/email/phone).
If your UX needs to know “can we tell the seller their offer will be eligible?” before asking for contact info, the best you can do is poll for a non-DENIED PENDING state after all home-detail answers are in. A PENDING status after home details means “no disqualifying answer surfaced; pricing is the remaining unknown.”
Related references
- Denial codes reference — the full enum and when each code fires.
- Choosing an offer-request query — which
getOffervariant to use for status / pricing reads. - DtcOnboardingFlow — the SDK component that drives the questionnaire end-to-end.