Skip to content

Choosing an offer-request query

The partner GraphQL API exposes three queries for reading the state of an offer request. They return different shapes; picking the right one depends on whether you need pricing, and whether you need multiple products.

At a glance

QueryReturnsUse when
offerRequestofferStatus, denialInfo, basic offerData (no pricing)You only need to poll the offer’s lifecycle status (PENDING / OFFERED / DENIED) without computing pricing every poll.
offerRequestWithSellDirectPricingSame as above plus a flat Sell Direct pricing object with net proceeds, mortgage balance, etc.Cash-only / Sell Direct integration. You don’t ship CNML to your sellers.
offerRequestWithProductsPricingSame plus per-product blocks: sellDirect and cnml, each with its own offerStatus, denialInfo, and offerData.Multi-product: Sell Direct plus optional CNML (Cash Now, More Later / Sell With Upside). Recommended even if you don’t ship CNML yet.

The server SDK methods are named correspondingly: getOffer / getOfferWithSellDirectPricing / getOfferWithProductsPricing in Node, get_offer / get_offer_with_sell_direct_pricing / get_offer_with_products_pricing in Ruby.

Decision tree

  • Polling for status only (e.g. a backend job waiting for OFFERED) → use offerRequest. Smallest payload.
  • Cash-only / Sell Direct only → either offerRequestWithSellDirectPricing (flatter, one level less of nesting) or offerRequestWithProductsPricing (more future-proof — ignore the cnml block if you don’t use it). Both work; the …WithProducts shape gives you a clean upgrade path if you add CNML later.
  • Multi-product (Sell Direct + CNML)offerRequestWithProductsPricing.

See the multi-product offer pricing guide for a full walkthrough of the Products variant, and the API reference for full field shapes.

Partial denials: the top-level denialInfo gotcha

On offerRequestWithProductsPricing, the top-level denialInfo is populated only when every product is denied. If even one product (sellDirect or cnml) is OFFERED or still being evaluated, the top-level denialInfo will be null — even though another product on the same response is denied with a reason.

This bites partner integrations that check the top-level denialInfo and assume “no denial.” The denied product’s reason still lives in its per-product block.

Correct check pattern

const result = await opendoor.getOfferWithProductsPricing(offerId);
// Top-level denialInfo: only set when ALL products are denied.
if (result.denialInfo) {
// Fully denied — no product is being offered.
handleFullDenial(result.denialInfo);
return;
}
// Sell Direct outcome.
if (
result.sellDirect?.offerStatus === 'OFFERED' &&
result.sellDirect.offerData
) {
presentSellDirect(result.sellDirect.offerData);
} else if (result.sellDirect?.denialInfo) {
// Sell Direct denied, but the seller may still be eligible for CNML.
noteSellDirectDenied(result.sellDirect.denialInfo);
}
// CNML outcome (null when partner does not ship CNML).
if (result.cnml?.offerStatus === 'ELIGIBLE' && result.cnml.offerData) {
presentCnml(result.cnml.offerData);
} else if (result.cnml?.denialInfo) {
noteCnmlDenied(result.cnml.denialInfo);
}

Wrong check pattern

const result = await opendoor.getOfferWithProductsPricing(offerId);
// ⚠️ Misses partial denials: if sellDirect is OFFERED and cnml is DENIED,
// result.denialInfo is null, so you'll silently present CNML as still
// in-flight even though it's been denied.
if (result.denialInfo) {
handleDenial(result.denialInfo);
} else {
presentBothProducts(result);
}

Rule of thumb: on the Products query, never trust top-level denialInfo alone. Always check sellDirect.denialInfo and cnml.denialInfo individually.

The single-product offerRequestWithSellDirectPricing query doesn’t have this concern — there’s only one product, so its denialInfo is unambiguous.

Polling cost

If you’re polling at a high rate (e.g. waiting for the offer to flip from PENDING to OFFERED), offerRequest is the cheapest call — it skips the pricing computation entirely. Use it for the wait loop, then switch to a pricing query once offerStatus === 'OFFERED'.