Skip to content

Denial codes reference

When an offer request is denied, the partner GraphQL API surfaces a denial code on denialInfo.code. This page is the canonical reference for which codes you can see, when in the offer lifecycle they fire, and how to write defensive code against the set.

The partner-visible enum

These are the only values that can appear in denialInfo.code on the partner GraphQL API. Internally we have a longer enum (~75 codes); the partnership BFF maps that down to this curated subset before responding, so partners don’t see internal-only states.

export const PARTNER_DENIAL_CODES = [
// Address / collision (only fire at create — see "When codes fire" below)
'ACTIVE_OFFER',
'IN_CONTRACT_WITH_OPENDOOR',
'LISTED_BY_OPENDOOR',
'OWNED_BY_OPENDOOR',
'PREPARING_OFFER',
// Region / address eligibility
'ADDRESS',
'PRODUCT_INACTIVE',
// Property characteristics (parcel / public-record derivable)
'AGE_RESTRICTED_COMMUNITY',
'BEFORE_1960',
'DWELLING_TYPE',
'FLOOD_HISTORY',
'FLOOD_ZONE',
'GATED_COMMUNITY',
'GUARD_GATED_COMMUNITY',
'LOT_SQ_FT',
'NON_BUYBOX_DWELLING_TYPE',
'SHARED_LOT',
'YEAR_BUILT',
// Property condition / disclosure (answer-driven)
'ASBESTOS_SIDING',
'BELOW_MARKET_RATE_OWNERSHIP',
'CESSPOOL',
'FIRE_DAMAGE',
'FOUNDATION',
'INELIGIBLE_HOA',
'LEASED_SOLAR_PANELS',
'POLYBUTYLENE_PIPING',
'PRIVATE_POOL',
'SEPTIC',
'TENANT_OCCUPIED',
'UNDERGROUND_OIL_TANK',
'UNIQUE_OWNERSHIP_STRUCTURE',
'UNPERMITTED_ADDITION',
'WELL_WATER',
// Valuation / pricing
'EXPECTED_LGM',
'LACK_OF_ADEQUATE_COMPS',
'VALUATION_TOO_HIGH',
'VALUATION_TOO_LOW',
// Seller / lifecycle
'MLS_LISTING_STATUS',
'SALE_TIMELINE',
// Catch-alls — always handle these as fallback
'OTHER_DENIAL_DETAIL',
'OTHER_DENIAL_GENERAL',
] as const;
export type PartnerDenialCode = (typeof PARTNER_DENIAL_CODES)[number];

When codes fire by stage

Different codes can fire at different points in the offer request lifecycle. Knowing this helps you decide where in your flow to surface a denial (and whether to short-circuit early).

At offerRequestCreate (address-only)

These codes can fire as soon as the seller submits an address — before any home-detail or contact-info answers come in. They evaluate against region data, public records, and parcel data.

Address-collision codes (these only fire at create — they never re-fire on later updates):

  • ACTIVE_OFFER — an active offer already exists for this address/customer.
  • IN_CONTRACT_WITH_OPENDOOR — the property is currently in contract with Opendoor.
  • LISTED_BY_OPENDOOR — Opendoor is the listing brokerage.
  • OWNED_BY_OPENDOOR — Opendoor owns the property.
  • PREPARING_OFFER — an offer request is already in flight for this address.

Region / buybox codes (also can fire later as data lands, but the address-only subset will hit here):

  • ADDRESS — out of service area.
  • PRODUCT_INACTIVE — the product isn’t active in this market.
  • DWELLING_TYPE, LOT_SQ_FT, YEAR_BUILT, FLOOD_ZONE, FLOOD_HISTORY, GATED_COMMUNITY, AGE_RESTRICTED_COMMUNITY — derivable from parcel / public-record data.

At offerRequestUpdate (as answers land)

Codes that fire as soon as an eligibility-relevant answer is submitted. No contact info required — these come back on the same offerRequestUpdate response that delivers the disqualifying answer.

  • LEASED_SOLAR_PANELS — solar disclosure.
  • FOUNDATION — known foundation issues.
  • FIRE_DAMAGE, ASBESTOS_SIDING, POLYBUTYLENE_PIPING, UNPERMITTED_ADDITION — condition disclosures.
  • INELIGIBLE_HOA — HOA structure not eligible.
  • TENANT_OCCUPIED, BELOW_MARKET_RATE_OWNERSHIP, UNIQUE_OWNERSHIP_STRUCTURE — ownership structure.
  • SEPTIC, WELL_WATER, UNDERGROUND_OIL_TANK, CESSPOOL, PRIVATE_POOL — site characteristics.
  • SALE_TIMELINE — the seller’s timeline doesn’t fit the product.
  • MLS_LISTING_STATUS — the property’s MLS status is incompatible.

The address-collision codes (above) do not re-fire on update — they’re checked once at create and that’s it.

After contact info (pricing pipeline)

These fire after the seller has submitted name/email/phone and the pricing pipeline has run. They depend on valuation/comp data that the pipeline computes; they don’t fire earlier.

  • VALUATION_TOO_HIGH, VALUATION_TOO_LOW — outside the buybox value range.
  • LACK_OF_ADEQUATE_COMPS — insufficient comparables to value confidently.
  • EXPECTED_LGM — expected levered gross margin doesn’t clear the bar.
  • OTHER_DENIAL_DETAIL — often surfaces UNABLE_TO_VALUE from the AVM (mapped to this generic external code).

For the full lifecycle from offerRequestCreate through pricing, see the offer-request lifecycle guide.

Handling catch-alls

OTHER_DENIAL_GENERAL and OTHER_DENIAL_DETAIL are intentional collapsing buckets — about a dozen internal codes get mapped to one or the other before being returned to partners. Examples of what falls into these:

  • OTHER_DENIAL_GENERALAGENT_RELATION, BUYER_UNAVAILABLE, DUPLICATE_LEAD, COVID_19, several deprecated experiment-related codes.
  • OTHER_DENIAL_DETAILUNABLE_TO_VALUE, OFFER_OUTLIER, miscellaneous risk-detail codes.

Always include a fallback branch in your switch. Code that handles only the codes it explicitly knows about will eventually fall off when we add a new internal denial that maps to one of these catch-alls.

function denialMessage(code: PartnerDenialCode): string {
switch (code) {
case 'LEASED_SOLAR_PANELS':
return 'Homes with leased solar panels are not currently eligible.';
// … your specific cases …
case 'OTHER_DENIAL_GENERAL':
case 'OTHER_DENIAL_DETAIL':
default:
// Use denialInfo.explanation for the human-readable reason.
return 'Opendoor cannot offer on this home at this time.';
}
}

The denialInfo object also includes an explanation string — partner-presentable copy that’s safe to show to the seller. Lean on it for the catch-all and unknown-code paths.

Defensive parsing

We add new codes occasionally — typically when a new internal denial reason is curated and added to the partner mapping. Your code should:

  • Treat unknown enum values as a generic denial (display denialInfo.explanation, log the unknown code).
  • Not assume the enum list is exhaustive at compile time. If you’re using TypeScript, ensure your switch has a default branch.
  • Reference this page when adding new code paths to make sure you’re handling the current set.