Skip to content

Build a stock feed

This guide is for marketplaces, comparison sites and dealer-group sites that show a Vyro dealer’s stock. By the end you will have a feed that lists the right vehicles, prices them for a state, shows the hero image, links to the dealer’s page, and stays in sync.

A vehicle should appear on your site when all of these are true. This is the same rule the dealer’s own Vyro website uses.

Field Value Why
showroom_stocked_vehicles.showroom_id the dealer’s showroom One vehicle can be shown by several showrooms.
is_published true The dealer has made it public.
is_listed true It should appear in lists and search. Unlisted vehicles are reachable only by direct link.
is_sold false Sold vehicles stay published for a while so links still work.
pricing_codes contains your state, e.g. AU_VIC The vehicle is priced (and sold) in that state.
listing_type retail Excludes private (consumer-to-consumer) listings.
  1. List vehicles for one state. Ask for the state’s price with pricing_config(path: $state), the featured image and the fields a card needs.

    query StockFeed($showroomId: uuid!, $state: String!, $limit: Int!, $offset: Int!) {
    stocked_vehicles(
    where: {
    showroom_stocked_vehicles: { showroom_id: { _eq: $showroomId } }
    is_published: { _eq: true }
    is_listed: { _eq: true }
    is_sold: { _eq: false }
    listing_type: { _eq: retail }
    pricing_codes: { _contains: [$state] }
    }
    order_by: [{ updated_at: desc }, { id: asc }]
    limit: $limit
    offset: $offset
    ) {
    id
    code
    stock_id
    vin
    condition
    production_year
    make
    model
    badge
    series
    body_type
    fuel_type
    transmission
    colour
    odometer
    hide_odometer
    updated_at
    price: pricing_config(path: $state)
    location {
    name
    address
    }
    hero: assets(
    where: { is_public: { _eq: true }, placement: { _eq: "featured" } }
    limit: 1
    ) {
    src
    alt
    }
    gallery: assets(
    where: { is_public: { _eq: true } }
    order_by: { order: asc }
    limit: 20
    ) {
    src
    alt
    placement
    }
    }
    total: stocked_vehicles_aggregate(
    where: {
    showroom_stocked_vehicles: { showroom_id: { _eq: $showroomId } }
    is_published: { _eq: true }
    is_listed: { _eq: true }
    is_sold: { _eq: false }
    listing_type: { _eq: retail }
    pricing_codes: { _contains: [$state] }
    }
    ) {
    aggregate {
    count
    }
    }
    }
    { "showroomId": "9b1d4e7a-2c3f-4a5b-8d6e-7f8091a2b3c4", "state": "AU_NSW", "limit": 50, "offset": 0 }
  2. Show the price. price.driveAwayPrice is in cents. Read the two flags before you print it. See Prices.

    function priceLabel(price, locale = 'en-AU', currency = 'AUD') {
    if (!price || price.isVariable) return 'Price on application';
    const amount = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    maximumFractionDigits: 0,
    }).format(Math.ceil(price.driveAwayPrice / 100));
    return price.isExclOnRoads ? `${amount} excl. on-road costs` : `${amount} drive away`;
    }
  3. Show the image. Use hero[0], falling back to gallery[0]. See Images for resizing.

  4. Link to the dealer. Send shoppers to the vehicle’s page on the dealer’s site, with the state so they see the same price. See Vehicle URLs.

    const url = `https://${showroom.domain}/vehicles/stocked/${vehicle.code}?region_id=${state}`;
  5. Page through the rest. Repeat with offset += limit until you have total.aggregate.count vehicles. See Pagination.

Dealers change prices, sell cars and add photos all day. Poll for changes rather than re-downloading everything.

Every 15 minutes, fetch what changed. Ask for vehicles updated since your last sync, without the live-stock filters, so you also see vehicles that were sold, unpublished or removed. Then upsert the live ones and remove the rest.

query Changes($showroomId: uuid!, $since: timestamptz!, $state: String!) {
stocked_vehicles(
where: {
showroom_stocked_vehicles: { showroom_id: { _eq: $showroomId } }
updated_at: { _gt: $since }
}
order_by: [{ updated_at: asc }, { id: asc }]
limit: 200
) {
id
updated_at
is_published
is_listed
is_sold
is_trashed
pricing_codes
price: pricing_config(path: $state)
}
}
  • Save the largest updated_at you processed as the next $since. Timestamps have microseconds, so keep the full string. Don’t round-trip it through JavaScript’s Date, which keeps only milliseconds and makes you re-read rows.
  • If you get exactly limit rows, go again from the last updated_at before you sleep. Pagination shows how to do this without skipping rows.
  • A vehicle that is sold, unlisted, unpublished, trashed or no longer priced in your state should come off your site.

Once a day, reconcile. Fetch the id of every live vehicle and remove anything you hold that is not in the list. This catches a vehicle that was removed from the showroom entirely, which a change feed can’t show you.

query LiveIds($showroomId: uuid!, $state: String!, $offset: Int!) {
stocked_vehicles(
where: {
showroom_stocked_vehicles: { showroom_id: { _eq: $showroomId } }
is_published: { _eq: true }
is_listed: { _eq: true }
is_sold: { _eq: false }
listing_type: { _eq: retail }
pricing_codes: { _contains: [$state] }
}
order_by: { id: asc }
limit: 500
offset: $offset
) {
id
}
}
  • Scoped every query to the showroom, and to one state.
  • Prices divided by 100, rounded up, and labelled “drive away” or “excl. on-road costs”.
  • “Price on application” when isVariable is true.
  • Vehicle titles built from year, make, model and badge, not the title field. Show badge as the dealer wrote it.
  • Odometer hidden when hide_odometer is true.
  • Images requested at the size you display.
  • Links go to the dealer’s vehicle page with ?region_id=.
  • Token cached for its 60 minutes, polling no more than every few minutes.