PropRaven Go SDK
Typed client for the PropRaven API, generated from its OpenAPI 3.1 spec. Standard library only, Go 1.22+. Go has no central registry: tags in the repo are the releases.
The current release was generated from OpenAPI 1.2.0 and wraps 70 of the 88 operations in the live spec (version 1.2.1). These 18 operations are not in the SDK yet; call them over REST with the same API key:
GET /api/v1/data/databricks/grantGET /api/v1/intelligence/runs/{runId}GET /api/v1/intelligence/runs/{runId}/handoffGET /api/v1/licensees/firmsGET /api/v1/licensees/individualsGET /api/v1/market/zillow/compareGET /api/v1/market/zillow/contextGET /api/v1/market/zillow/timeseriesGET /api/v1/neighborhood/distressGET /api/v1/parcels/{id}/distressGET /api/v1/parcels/{id}/signalsGET /api/v1/parcels/{id}/tax-statusGET /api/v1/parcels/{id}/zoningGET /api/v1/permits/{permitId}/eventsGET /api/v1/r2-credentialsGET /api/v1/zoning/coverageGET /api/v1/zoning/searchPOST /api/v1/intelligence/scenarios
Install
go get github.com/jdw2111/propraven-go@latestQuickstart
package main
import (
"context"
"fmt"
"log"
propraven "github.com/jdw2111/propraven-go"
)
func main() {
client := propraven.NewClient() // reads PROPRAVEN_API_KEY
ctx := context.Background()
parcel, err := client.Parcels.Get(ctx, "37:119:12104406", nil)
if err != nil {
log.Fatal(err)
}
// Nullable fields are pointers: nil when the record has no owner or no value.
owner, value := "(no owner of record)", "(no assessed value)"
if parcel.OwnerName != nil {
owner = *parcel.OwnerName
}
if parcel.TotalAssessedValue != nil {
value = fmt.Sprint(*parcel.TotalAssessedValue)
}
fmt.Println(owner, value)
permits, err := client.Parcels.Permits(ctx, "37:119:12104406",
&propraven.ParcelsPermitsParams{Shape: propraven.String("envelope")})
if err != nil {
log.Fatal(err)
}
if permits.Object != nil {
fmt.Println(permits.Object.PermitCount, "permits")
}
}Every nullable field is a pointer (for example OwnerName *string, TotalAssessedValue); check it for nil before dereferencing, since parcels with no owner or no assessed value are common.
Options: propraven.WithAPIKey, WithBaseURL, WithHTTPClient, WithMaxRetries, WithTimeout, WithHeader; any option can also be passed to a single call.
The default base URL is https://propraven.com (every path starts with /api/v1); override it withPROPRAVEN_BASE_URL. Parcel ids take the canonicalstate:county:parcel form, the legacy 5-digit form or a PropRaven UUID.
Errors
The API answers every error with an RFC 7807 problem body; the SDK raises a typed error per status (400, 401, 402, 403, 404, 405, 409, 413, 422, 429, 503, 504, other 5xx) carryingcode, detail, the per-parameter errors and the request id.
_, err := client.Parcels.Get(ctx, "37:119:00000000000", nil)
switch {
case propraven.IsNotFound(err):
// 404
case propraven.IsRateLimited(err):
// 429; see err.(*propraven.Error).RetryAfter
case propraven.IsPaymentRequired(err):
// 402; the x402 offer is in .Accepts
}
var apiErr *propraven.Error
if errors.As(err, &apiErr) {
fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.Detail, apiErr.RequestID)
}Retries, timeouts and rate limits
Network errors, 429, 503 and 504 are retried twice by default (other 5xx only for GET and DELETE), waiting for Retry-After or the rate-limit reset when the server names one, otherwise backing off exponentially. A wait longer than 60 seconds is raised instead of slept, and a 402 is never retried.
client := propraven.NewClient(
propraven.WithMaxRetries(3),
propraven.WithTimeout(30*time.Second),
)
fmt.Println(client.LastRateLimit())Pagination
it := client.Deals.AbsenteeIter(ctx,
&propraven.DealsAbsenteeParams{CountyFIPS: propraven.String("37119")},
propraven.IterOptions{PageSize: 100, MaxItems: 1000})
for it.Next() {
row := it.Current()
fmt.Println(row.ParcelID)
}
if err := it.Err(); err != nil {
log.Fatal(err)
}Webhook signature verification
Checks X-PropRaven-Signature (t=<unix_ms>,v1=<hex>, HMAC-SHA256 overt.raw_body) in constant time with a 5-minute replay window. A dependency-free verifier is on thewebhooks page.
import (
"io"
"net/http"
"os"
propraven "github.com/jdw2111/propraven-go"
)
func handler(w http.ResponseWriter, r *http.Request) {
rawBody, _ := io.ReadAll(r.Body) // the exact request body, before any JSON parsing
event, err := propraven.VerifyWebhook(rawBody,
r.Header.Get("X-PropRaven-Signature"), os.Getenv("PROPRAVEN_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
_ = event // json.RawMessage: unmarshal into your own struct; the name is in event_type
w.WriteHeader(http.StatusNoContent)
}