Zillow API Golang example

Zillow API in Go

Call a Zillow data API from Go with net/http, decode the response into typed structs that keep null distinct from zero, and batch many homes in one request.

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 Go fits

Go's encoding/json decodes a JSON null into the zero value unless the field is a pointer. For property data that matters: a price of zero and a price Zillow doesn't show are different facts. The structs below use pointers for every value that can be null, so your code has to check before it uses one.

Use a context with a deadline for each request instead of a client-wide timeout, so a batch can wait longer than a single lookup. A live lookup takes typically 8 seconds, up to about 20 seconds.

Only the fields you declare are decoded. Start with the handful you need and add more from the field reference later; the response has many more, including the calculated figures under data.calculated.

Types for the fields you use

Pointers for anything nullable. Calculated is the data.calculated object.

Go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

const base = "https://api.propertiesapi.com/v1"

type Home struct {
	Zpid          *string  `json:"zpid"`
	DetailURL     string   `json:"detailUrl"`
	Address       string   `json:"address"`
	HomeStatus    *string  `json:"homeStatus"`
	Price         *float64 `json:"price"`
	Zestimate     *float64 `json:"zestimate"`
	RentZestimate *float64 `json:"rentZestimate"`
	Beds          *float64 `json:"beds"`
	Baths         *float64 `json:"baths"`
	LivingArea    *float64 `json:"livingArea"`
	Calculated    struct {
		GrossYieldPct *float64 `json:"gross_yield_pct"`
		DaysOnMarket  *int     `json:"days_on_market"`
	} `json:"calculated"`
}

type Meta struct {
	Credits int  `json:"credits"`
	Cached  bool `json:"cached"`
}

type APIError struct {
	Status  int
	Code    string         `json:"code"`
	Message string         `json:"message"`
	Details map[string]any `json:"details"`
}

func (e *APIError) Error() string { return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message) }

One request function

Sends the key, decodes the body into out, and turns an error body into an *APIError.

Go
func call(ctx context.Context, method, path string, q url.Values, body string, out any) error {
	u := base + path
	if q != nil {
		u += "?" + q.Encode()
	}
	req, err := http.NewRequestWithContext(ctx, method, u, strings.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("x-api-key", os.Getenv("PROPERTIESAPI_KEY"))
	if body != "" {
		req.Header.Set("content-type", "application/json")
	}
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		var e struct{ Error APIError `json:"error"` }
		json.NewDecoder(res.Body).Decode(&e)
		e.Error.Status = res.StatusCode
		return &e.Error
	}
	return json.NewDecoder(res.Body).Decode(out)
}

Look up one home

Exactly one of url, zpid or address. Check every pointer before you use it.

Go
func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
	defer cancel()

	var r struct {
		Data Home `json:"data"`
		Meta Meta `json:"meta"`
	}
	err := call(ctx, "GET", "/zillow/property", url.Values{"address": {"2640 Steiner St, San Francisco, CA 94115"}}, "", &r)
	if apiErr, ok := err.(*APIError); ok && apiErr.Code == "not_found" {
		fmt.Println("no matching Zillow property (not billed)")
		return
	} else if err != nil {
		panic(err)
	}
	fmt.Println(r.Data.Address, deref(r.Data.HomeStatus))
	if r.Data.Price != nil {
		fmt.Printf("list price $%.0f\n", *r.Data.Price)
	}
	if r.Data.Zestimate == nil {
		fmt.Println("Zillow shows no Zestimate for this home")
	}
	fmt.Println("cached:", r.Meta.Cached, "credits:", r.Meta.Credits)
}

func deref(s *string) string {
	if s == nil {
		return "unknown"
	}
	return *s
}

Batch up to 50 links or addresses

Each item comes back with ok, and either data or error. Decode both shapes into one struct.

Go
type BatchItem struct {
	Index int       `json:"index"`
	OK    bool      `json:"ok"`
	Data  *Home     `json:"data"`
	Error *APIError `json:"error"`
}

func batch(items []string) ([]BatchItem, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 320*time.Second)
	defer cancel()
	body, _ := json.Marshal(map[string]any{"items": items})
	var r struct {
		Data []BatchItem `json:"data"`
		Meta struct {
			Credits   int `json:"credits"`
			CacheHits int `json:"cache_hits"`
		} `json:"meta"`
	}
	if err := call(ctx, "POST", "/zillow/batch", nil, string(body), &r); err != nil {
		return nil, err
	}
	return r.Data, nil
}

Go gotchas

  • A plain float field without a pointer turns a null price into zero. Use pointers, or json.RawMessage if you want to decide later.
  • http.DefaultClient has no timeout of its own; the context deadline is what stops a request. Give batches a longer deadline than single lookups.
  • Rate limits and busy answers (rate_limited, upstream_busy) carry retry_after in error.details. Sleep that long before retrying.
  • Set a client timeout of at least 120 seconds: an occasional lookup takes longer.

Zillow API and Go: questions

Is there a Go module for the Zillow API?+

Not from us yet. The API is two HTTPS endpoints, so the standard library code on this page is a complete client.

Can I run lookups concurrently from goroutines?+

Yes, within your plan's rate limit, which counts every key and batch item on your account together. For lists, one batch request is simpler than many goroutines.

How do I avoid paying twice for the same home?+

Repeat lookups of the same home within 24 hours come from our cache and are free. Keep the zpid and detailUrl you get back and look homes up by link next time; links are the most precise input.

What does a lookup cost?+

Zillow links and addresses cost the same: 1 credit per successful lookup. You start with 50 free lookups.

Make your first call from Go

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.