Best Buy Stock & Pickup Availability

Site bestbuy.comTask check-stock-muk8toVersion v1Updated Jul 13, 2026Category retail

Given a Best Buy SKU or product URL (and optional ZIP), return current price, online Ship-to-Home availability with ETA, pickup availability at nearby stores within radius, plus product title/brand/model/limit notice. Read-only — never adds to cart or reserves. This skill was captured from a live agent session on bestbuy.com and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Given a Best Buy SKU (numeric product ID, e.g. 6418599) or a product URL — and optionally a ZIP code or store ID — return the product's current availability across fulfillment channels: online Ship-to-Home (with ETA), Pickup at nearby stores (per-store availability within radius_miles of the ZIP, with per-store ready-time), and the product's metadata (title, brand, model, current price, member-pricing tier, any "Limit X per customer" notice). Read-only — never click Add to Cart, Pick up at Store, Sign In, Add Protection, or any purchase / reservation control.

When to Use

  • A user asks "is this in stock at my local Best Buy / for shipping to my ZIP?"
  • Price-monitoring / availability-watch flows over a list of SKUs.
  • Cross-retailer stock comparison (Amazon + Best Buy + Target).
  • Verifying a deal is still bookable before notifying the user.
  • Use the sibling skill bestbuy.com/search-products to resolve a query to a list of SKUs first; this skill only takes a known SKU/URL.

Workflow

Best Buy's PDP is a Next.js App Router page guarded by Akamai Bot Manager (_abck + bm_* cookie set). The entire fulfillment data shape (price, online + pickup + delivery availability, button state) is embedded in the SSR HTML as Apollo Client cache events under window[Symbol.for("ApolloSSRDataTransport")] — you do not need to wait for React to hydrate. Lead with a stealth+residential-proxy browser session, navigate once, then parse the static HTML.

Best Buy's developer API does not expose live store-level stock. The bestbuy.com/gateway/graphql endpoint is the canonical source the site itself uses (operation: FulfillmentOptionHook_FulfillmentDynamicQuery). Direct cookieless POSTs from a fresh IP are likely Akamai-blocked — treat the GraphQL endpoint as a candidate fast-path (see Browser fallback at end), not the recommended route.

1. Stealth + residential-proxy session — mandatory

Run the whole flow through a single browserless_agent call with a top-level residential proxy arg (stealth is built in):

// browserless_agent, top-level arg:
"proxy": { "proxy": "residential", "proxyCountry": "us" }

A plain call with no residential proxy gets Akamai 403 / Access-Denied HTML on the first navigation. The proxy is required (stealth is always on), so keep the entire set-ZIP-cookie → navigate → extract flow inside ONE browserless_agent commands array. The session persists across calls, keyed by the proxy config, so a later call repeating the same proxy reconnects to the same cookies/session; batching into one call is simply the easy way to avoid accidentally dropping that config.

2. Set the user's ZIP before navigation

Without a user ZIP, Best Buy's SSR populates fulfillment data using the request-IP geolocation, falling back to ZIP 55423 / store 7 (Richfield, MN — Best Buy HQ) when geo is ambiguous. To get the data scoped to the user's ZIP, set the locDestZip cookie on the session before opening the PDP:

// First command in the browserless_agent commands array:
{
  "method": "goto",
  "params": {
    "url": "https://www.bestbuy.com/",
    "waitUntil": "load",
    "timeout": 45000,
  },
}
// Then set the locDestZip cookie, or drive the location-picker UI:
// click "Update location" in the header → type the ZIP → submit, then wait load. Then proceed.

The cookie that drives fulfillment context is locDestZip (the ZIP). locStoreId is the resolved primary store. Both auto-populate after a successful zip-picker submission. Verify by reading document.cookie after the picker dismisses.

3. Navigate to the canonical PDP URL

Two URL schemes exist; both work but only one is canonical post-2024:

SchemeExampleBehavior
Legacyhttps://www.bestbuy.com/site/{sku}.p?skuId={sku}301-redirects to canonical
Canonical (current)https://www.bestbuy.com/product/{slug}/{bsin}Direct PDP — preferred

The bsin is a 10-character alphanumeric ID (e.g. JJ8ZHP82P6). If you only have the SKU, navigate the legacy URL (the browser follows the 301 automatically) and read the final URL to learn the canonical one — but note the redirect-leak gotcha (see below: an unknown/inactive SKU may redirect to a totally unrelated product).

// Next commands in the same commands array:
{ "method": "goto", "params": { "url": "https://www.bestbuy.com/site/{SKU}.p?skuId={SKU}", "waitUntil": "load", "timeout": 45000 } },
{ "method": "waitForTimeout", "params": { "time": 1500 } }   // Apollo SSR events finish streaming after load

4. Extract product metadata from JSON-LD

Parse <script id="product-schema" type="application/ld+json"> in-page with an evaluate command (returns under .value). You can also grab { "method": "html", "params": { "selector": "body" } } and parse the string yourself, but folding it into evaluate avoids shipping the whole DOM:

// Next command in the same commands array:
{
  "method": "evaluate",
  "params": {
    "content": "(()=>{ const el = document.getElementById('product-schema'); return el ? el.textContent : null; })()",
  },
}

Then JSON.parse the returned .value.

You get a structured Product object with name, sku, model, brand.name, color, url, image, aggregateRating { ratingValue, reviewCount }, additionalProperty[] (full spec sheet), and offers[]. Caveat: for dotComDisplayStatus: "inactive" products (discontinued online), offers[] is empty or contains only a Refurbished/Open-Box offer — the active "new" price lives in the Apollo cache (step 5).

5. Extract price + fulfillment from the Apollo SSR cache

The richest signal is in inline <script> tags that hydrate Apollo's cache via window[Symbol.for("ApolloSSRDataTransport")] ??= []).push(...). Each push contains a stream of {type: "started" | "next" | "completed", options/value, id} events. The relevant events are the type: "next" payloads whose value.data.productBySkuId is populated. Specifically look for:

Source queryFields to extract
InactiveProductHeader_Init / PDP_ProductSkuIdComposite_InitproductBySkuId.{brand, skuId, name.short, manufacturer.modelNumber, primaryImage.piscesHref, dotComDisplayStatus, bsin, upc}
FulfillmentOptionHook_FulfillmentDynamicQueryproductBySkuId.price.customerPrice, productBySkuId.fulfillmentOptions.{buttonStates[], shippingDetails[], deliveryDetails[], ispuDetails[]}

A robust extractor regexes for "productBySkuId":\{[\s\S]*?\}\} and "fulfillmentOptions":\{[\s\S]*?\} after stripping JS quoting, then JSON.parse on the surrounding object. The hydration stream lives inside large inline script tags; don't try to parse it as standalone JSON — find the inner object literals.

6. Map the data to the output schema

  • Online (Ship-to-Home) state — derive from fulfillmentOptions.buttonStates[].buttonState:
    • ADD_TO_CART / BUY_NOW"In Stock"
    • SOLD_OUT"Sold Out"
    • COMING_SOON"Coming Soon" (read releaseDateDisplayValue for the date)
    • NOT_AVAILABLE"Currently Unavailable"
    • CHECK_STORES → online not available, pickup may be — fall through to step 6 pickup logic
  • Ship-to-Home ETA — from shippingDetails[].shippingAvailability[].customerLOSGroup you get displayDateType, minLineItemMaxDate, maxLineItemMaxDate, name (e.g. "Standard"). The user-facing "Get it by Wed, May 20" string is rendered server-side and also surfaces in the SSR HTML — search for "shippingDisplayDateType" and adjacent date-text fragments. shippingEligible: false ⇒ no Ship-to-Home.
  • Pickup stores — from ispuDetails[].nearbyLocations[]. Each entry has availability.{maxDate, minPickupInHours, pickupEligible, quantity, fulfillmentType} and store.{storeId, displayName, address, city, state, zip, distance}. Map:
    • pickupEligible: true && minPickupInHours <= 24"Available Today" with ready_time = "Ready in ${minPickupInHours} hour(s)"
    • pickupEligible: true && minPickupInHours > 24"Available Tomorrow" (or use maxDate) with ready_time = "Ready ${maxDate}"
    • pickupEligible: false"Not Available"
  • Price + member tierproductBySkuId.price.customerPrice is the price for the current planPaidMemberType (default "NULL" = logged-out / non-member). To get member-tier pricing (My Best Buy Plus / Total), re-execute the same query with planPaidMemberType: "PLUS_NEW" or "TOTAL" — these are typically only visible to members, but the SSR may render member-only prices in adjacent priceCondition blocks.
  • "Limit X per customer" — search the rendered HTML for Limit\s+\d+\s+per\s+customer. This is a presentational string near the price block; it does not live in the GraphQL response.

7. (Optional) Filter by radius_miles

The default inStorePickup.searchNearby: true returns the closest ~10 stores. Best Buy's default radius is 25 miles. To enforce a custom radius_miles, post-filter the returned nearbyLocations[] by .store.distance (the field is miles as a number). To force a wider search, the only verified way is via the live "Find a store" UI — there's no searchRadius field on ProductFulfillmentInput.inStorePickup exposed publicly. Document the cap in your output.

8. Session teardown

No session-release step, and nothing to release — the session persists across calls, keyed by the proxy config; it does not tear down when the commands array returns. Keeping steps 1–7 inside a single call's commands array is the convenient way to hold the Akamai cookies and ZIP context together; a later call that repeats the same proxy reconnects to that same warmed session, while one that drops or changes it lands in a different, cold session.

Browser fallback / candidate API path

If a future agent wants to skip step 3's page render entirely, the GraphQL endpoint can in principle be hit directly:

POST https://www.bestbuy.com/gateway/graphql
Content-Type: application/json
Origin: https://www.bestbuy.com
Referer: https://www.bestbuy.com/product/{slug}/{bsin}
Cookie: locDestZip={zip}; locStoreId={storeId}; _abck=...; bm_sz=...

Body (operationName: FulfillmentOptionHook_FulfillmentDynamicQuery, query text as observed in the SSR HTML's Apollo started events). This was not end-to-end verified in skill construction — direct cookieless POSTs from an untrusted IP are very likely Akamai-blocked (the _abck / bm_sz cookies are sensor-checked). The safe pattern is the same idea, in page context: browserless_agent (or browserless_function) navigates the PDP once with the residential proxy so the Akamai cookies land on the page, then — same origin, so egress is live after goto — an evaluate/page fetch(... , { credentials: "include" }) re-issues the GraphQL POST. Treat the GraphQL path as a 2x-faster optimization, not the primary path.

Site-Specific Gotchas

  • READ-ONLY. Do not click Add to Cart, Pick up at Store, Sign In, Add Protection, "Add Open-Box to Cart", or any reservation control. Capturing data is fine; transitioning state is not.
  • Akamai Bot Manager. A residential proxy (proxy: { proxy: "residential" }) is mandatory; stealth is always on in browserless_agent. A plain call with no residential proxy gets a 403 + Akamai's Access-Denied HTML or an interstitial. Cookies returned: _abck, bm_ss, bm_s, bm_so, bm_sz, akacd_PR_www_bestbuy_com, bby_cbc_lb — the _abck is the canonical Bot-Manager session token.
  • URL-migration redirect-leak gotcha. Best Buy migrated PDP URLs from /site/{sku}.p?skuId={sku} to /product/{slug}/{bsin}. The legacy URL still 301-redirects, but an unknown / deactivated SKU may redirect to a completely unrelated product instead of 404. Example observed during skill construction: legacy URL for SKU 6418599 returned 301 → MacBook Air M1 (which matches), but the redirect mechanism doesn't always preserve mapping. Always verify the redirected URL's bsin matches the SKU's expected canonical path by reading productBySkuId.skuId from the SSR HTML and comparing against the input SKU. If they mismatch, the SKU likely doesn't exist anymore — return success: false, reason: "sku_not_found".
  • dotComDisplayStatus: "inactive" is a distinct outcome. When a product is no longer sold online (only refurb / open-box), dotComDisplayStatus == "inactive", buttonStates[0].buttonState == "NOT_AVAILABLE", shippingAvailability[0].shippingEligible == false, ispuAvailability[0].pickupEligible == false, and JSON-LD offers[] is empty (or contains only refurbished). This is NOT an error — return online.availability: "Currently Unavailable" with the refurb offer (if any) under a refurbished_offer field.
  • Default ZIP is Richfield, MN (55423), default store is 7. Without a locDestZip cookie, the SSR renders fulfillment context against Best Buy's HQ ZIP. The PDP HTML will say "Get it by … to 55423" — silently emitting this to a user in California is a bug. Always verify shippingDetails[].destinationZipCode matches the user's input ZIP before emitting, and re-fetch with the right cookie if not.
  • Data is in the SSR HTML — do not wait for React. The Apollo SSR transport events stream all productBySkuId and fulfillmentOptions payloads inline during initial HTML render. A waitForTimeout of 1500ms after load is sufficient; React hydration is irrelevant. Don't reach for snapshot to find fulfillment text — the rendered React tree's text content is the same data you already have in window[Symbol.for("ApolloSSRDataTransport")] events, but harder to parse.
  • buttonState is the canonical availability signal, not visible button text. Observed enum values during construction: NOT_AVAILABLE, ADD_TO_CART, BUY_NOW, SOLD_OUT, COMING_SOON, CHECK_STORES. Map to user-facing strings yourself; don't trust scraped button labels (they're A/B-test-controlled).
  • Pickup data depends on inStorePickup.storeId AND searchNearby: true. The GraphQL fulfillmentInput.inStorePickup requires a storeId even when you want a nearby-search result — pass 7 (HQ store) or any valid store ID; with searchNearby: true Best Buy returns the actually-nearby stores based on destinationZipCode. Don't try to omit storeId.
  • radius_miles is not a publicly-exposed filter. ProductFulfillmentInput.inStorePickup has no searchRadius field. The default ~25-mile radius is server-controlled. Post-filter nearbyLocations[] by store.distance (a number in miles) to honor a custom radius.
  • PDP HTML for active products may exceed 1MB. A raw HTTP-fetch service that caps response bodies at 1MB will truncate active PDPs (with full review prerender, >1MB); inactive/discontinued PDPs (~900KB) squeak by. The browserless_agent browser path has no such cap — the html/evaluate read comes from the rendered DOM in-page. Prefer parsing in-page (evaluate) and returning a compact projection so you also stay under the ~200k-char text-return limit rather than shipping the whole HTML back.
  • Best Buy Developer API does not expose store-level stock. The https://api.bestbuy.com/v1/products(...) endpoint requires an API key and only returns catalog metadata + online availability. There is no documented endpoint for per-store pickup quantity. /gateway/graphql is the only source.
  • POST /gateway/graphql from cookieless IP is candidate-only, untested in skill construction. The construction environment couldn't validate direct POST behavior (sandbox DNS policy blocked outbound to bestbuy.com and to the remote browser's CDP connect endpoint). The skill assumes — but does not prove — that the GraphQL endpoint is Akamai-protected in the same way as the page. Future agents should re-verify before relying on the API fast-path.
  • GraphQL operation: FulfillmentOptionHook_FulfillmentDynamicQuery. Variables: skuId: String!, fulfillmentInput: ProductFulfillmentInput!, productPriceInput: ProductItemPriceInput!, openBoxCondition: Int. The full query text (with all fragments) is embedded in the SSR HTML as an Apollo started event — copy it from there rather than hand-writing it. Operation names and shapes were stable across two PDP fetches during construction; if Best Buy renames them, the SSR HTML always has the current shape.
  • Member-tier pricing is logged-in-only. customerPrice reflects planPaidMemberType: "NULL" (logged-out) by default. To surface My Best Buy Plus / Total prices, the page context needs an authenticated session — which violates read-only. Emit member_pricing.my_best_buy_plus = null, my_best_buy_total = null unless an authenticated session is explicitly in scope (out of scope for this skill).
  • Binary fetches (hero image / gzip) come back as bytes, not text. If you need the actual hero image (for a thumbnail), don't try to return it through a text/evaluate path — a raw HTTP fetch of a binary body hands you a base64 envelope, not decoded bytes. Use browserless_function returning a proper binary block ({ data, type: "image/png" }), or just surface the image URL from the JSON-LD image field. Not relevant for HTML/JSON extraction.

Expected Output

A single JSON object, with these distinct outcome shapes:

// Active product, online + pickup data populated for user's ZIP
{
  "success": true,
  "sku": "6418599",
  "bsin": "JJ8ZHP82P6",
  "title": "Sony - WH-1000XM5 Wireless Noise-Canceling Over-the-Ear Headphones - Black",
  "brand": "Sony",
  "model": "WH1000XM5/B",
  "upc": "027242923386",
  "url": "https://www.bestbuy.com/product/{slug}/{bsin}",
  "primary_image_url": "https://pisces.bbystatic.com/image2/BestBuy_US/images/products/6418/6418599_sd.jpg",
  "price": { "current": 399.99, "currency": "USD" },
  "member_pricing": { "my_best_buy_plus": null, "my_best_buy_total": null },
  "limit_per_customer": null,
  "online": {
    "availability": "In Stock",
    "button_state": "ADD_TO_CART",
    "ship_to_home_eta": "Get it by Wed, May 20",
    "destination_zip": "94103",
    "shipping_eligible": true
  },
  "pickup": {
    "zip": "94103",
    "radius_miles": 25,
    "stores": [
      {
        "store_id": "186",
        "store_name": "Harrison Street",
        "address": "1717 Harrison St, San Francisco, CA 94103",
        "distance_miles": 0.6,
        "availability": "Available Today",
        "min_pickup_in_hours": 1,
        "max_date": "2026-05-18",
        "quantity": 5,
        "ready_time": "Ready in 1 hour"
      }
    ]
  }
}

// Inactive / discontinued product (only refurb available)
{
  "success": true,
  "sku": "6418599",
  "bsin": "JJ8ZHP82P6",
  "title": "MacBook Air 13.3\" Laptop - Apple M1 chip - 8GB Memory - 256GB SSD - Gold",
  "brand": "Apple",
  "model": "MGND3LL/A",
  "dot_com_display_status": "inactive",
  "online": { "availability": "Currently Unavailable", "button_state": "NOT_AVAILABLE", "shipping_eligible": false },
  "pickup": { "zip": "55423", "radius_miles": 25, "stores": [] },
  "refurbished_offer": {
    "price": 364.99,
    "sku": "6489687",
    "condition": "Refurbished",
    "currency": "USD"
  }
}

// SKU redirected to an unrelated product (legacy-URL mismatch)
{ "success": false, "reason": "sku_not_found", "input_sku": "9999999", "redirected_to_sku": "6418599" }

// Akamai 403 / bot-detection wall on first navigation
{ "success": false, "reason": "akamai_blocked", "status_code": 403, "url": "https://www.bestbuy.com/product/..." }

// Sold out for the user's ZIP (online out, all nearby stores out)
{
  "success": true,
  "sku": "...",
  "online": { "availability": "Sold Out", "button_state": "SOLD_OUT", "shipping_eligible": false },
  "pickup": { "zip": "94103", "radius_miles": 25, "stores": [/* all with availability: "Not Available" */] }
}

// Coming Soon (pre-order)
{
  "success": true,
  "sku": "...",
  "online": {
    "availability": "Coming Soon",
    "button_state": "COMING_SOON",
    "release_date": "2026-06-15",
    "ship_to_home_eta": "Get it by Mon, Jun 15",
    "shipping_eligible": true
  },
  "pickup": { "zip": "...", "stores": [] }
}

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=bestbuy.com&task=check-stock-muk8to