Zillow API in Ruby
Get Zillow listing and property data into Ruby with Net::HTTP and JSON from the standard library, then move the lookups into a background job for Rails.
Base URL https://api.propertiesapi.com/v1 · auth header x-api-key. PropertiesAPI is an independent service that returns Zillow property and listing data as JSON, provided as-is.
How Ruby fits
Ruby's standard library already has everything the API needs: Net::HTTP for the request, URI for encoding the Zillow link or address, and JSON for the response. The client below is a small module you can drop into a Rails app's lib folder or a one-off script.
The main thing to get right is the timeout. Net::HTTP's default read timeout is lower than the timeout we recommend, and a live lookup takes typically 8 seconds, up to about 20 seconds. Set read_timeout explicitly, and in Rails run lookups in a background job rather than inside a web request.
Errors are raised as one exception class with the HTTP status and the API's error code, so a job can decide quickly whether to retry, give up or ask the user for a better address.
A small client module
Reads PROPERTIESAPI_KEY from the environment and returns the parsed body as a Hash.
require "net/http"
require "json"
require "uri"
module PropertiesAPI
BASE = "https://api.propertiesapi.com/v1"
class Error < StandardError
attr_reader :status, :code, :details
def initialize(status, code, message, details)
@status, @code, @details = status, code, details || {}
super("#{status} #{code}: #{message}")
end
end
def self.request(method, path, query: nil, body: nil, timeout: 120)
uri = URI("#{BASE}#{path}")
uri.query = URI.encode_www_form(query) if query
req = method == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["x-api-key"] = ENV.fetch("PROPERTIESAPI_KEY")
if body
req["content-type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: timeout) { |http| http.request(req) }
json = JSON.parse(res.body) rescue {}
unless res.is_a?(Net::HTTPSuccess)
e = json["error"] || {}
raise Error.new(res.code.to_i, e["code"] || "unknown", e["message"] || "request failed", e["details"])
end
json
end
def self.property(**input) = request(:get, "/zillow/property", query: input)
def self.batch(items) = request(:post, "/zillow/batch", body: { items: items }, timeout: 320)
endLook up a home
Pass url:, zpid: or address:. data is a Hash with string keys; meta tells you whether the answer came from the cache.
body = PropertiesAPI.property(address: "2640 Steiner St, San Francisco, CA 94115")
home = body["data"]
puts "#{home["address"]} (#{home["homeStatus"]})"
puts "List price: #{home["price"] || "not shown"}"
puts "Zestimate: #{home["zestimate"] || "not shown"}, rent Zestimate: #{home["rentZestimate"] || "not shown"}"
puts "Price drops this listing: #{home.dig("calculated", "price_drops_count") || "n/a"}"
puts body["meta"]["cached"] ? "cache hit (free)" : "live lookup"Retry only what is worth retrying
Rate limits and busy answers carry a retry_after wait in error.details. A not_found or address_ambiguous answer won't change on retry.
def find_home(**input)
attempts = 0
begin
PropertiesAPI.property(**input)["data"]
rescue PropertiesAPI::Error => e
case e.code
when "not_found" then nil # not billed
when "address_ambiguous"
warn "closest match: #{e.details.dig("candidate", "detailUrl")}"
nil
else
raise unless [429, 502, 503, 504].include?(e.status) && (attempts += 1) < 4
sleep(e.details["retry_after"] || 2**attempts)
retry
end
end
endBatch inside a background job
Send up to 50 items per batch request and save each result as it comes back. Failed items cost nothing; leave them for a later retry.
class RefreshHomesJob < ApplicationJob
def perform(listing_ids)
listings = Listing.where(id: listing_ids).to_a
listings.each_slice(50) do |slice|
body = PropertiesAPI.batch(slice.map(&:zillow_url))
body["data"].each do |r|
listing = slice[r["index"]]
if r["ok"]
listing.update!(price: r["data"]["price"], zestimate: r["data"]["zestimate"],
home_status: r["data"]["homeStatus"], refreshed_at: Time.current)
else
listing.update!(last_error: r["error"]["code"])
end
end
end
end
endRuby gotchas
- Net::HTTP raises Net::ReadTimeout when read_timeout is too short; that is a client-side timeout, and the lookup may still finish and be cached on our side. Retrying soon after is usually a free cache hit.
- JSON.parse gives string keys. Use home["price"], or pass symbolize_names: true if you prefer symbols.
- Fields Zillow doesn't show for a home come back as null, so never read null as zero.
- Set a client timeout of at least 120 seconds: an occasional lookup takes longer.
Zillow API and Ruby: questions
Is there a Zillow API gem?+
Gems written for Zillow's retired ZWSID API no longer work. With PropertiesAPI you don't need a gem: the standard library module above is the whole client.
Can I use Faraday or HTTParty instead?+
Yes. Send the same request: the x-api-key header, one of url, zpid or address as a query parameter, and a long read timeout.
Should lookups run inside a Rails controller?+
Better not. A live lookup can take longer than you want a web request to wait. Run it in a background job and show the saved result.
What does a lookup cost?+
Zillow links and addresses cost the same: 1 credit per successful lookup. Repeat lookups of the same home within 24 hours come from our cache and are free. You start with 50 free lookups.
Make your first call from Ruby
50 free lookups, no card, no approval.
Zillow is a trademark of Zillow, Inc. PropertiesAPI is an independent service and is not affiliated with, endorsed by or sponsored by Zillow.