REA Pricing Client

An Elixir client library for the REA Pricing API, built with the Req HTTP client.

Features

Installation

Add rea to your list of dependencies in mix.exs:

def deps do
  [
    {:rea, "~> 0.1.0"}
  ]
end

Configuration

The client uses application-level configuration where credentials are configured globally and shared across your entire application. Each environment (production, test, etc.) has its own dedicated TokenManager process.

Basic Configuration

Configure environments in your config/config.exs:

config :rea,
  environments: [
    prod: [
      base_url: "https://api.realestate.com.au",
      client_id: System.get_env("REA_PROD_CLIENT_ID"),
      client_secret: System.get_env("REA_PROD_CLIENT_SECRET")
    ]
  ]

Runtime Configuration

For production deployments, use config/runtime.exs:

import Config

if config_env() == :prod do
  config :rea,
    environments: [
      prod: [
        base_url: "https://api.realestate.com.au",
        client_id: System.fetch_env!("REA_PROD_CLIENT_ID"),
        client_secret: System.fetch_env!("REA_PROD_CLIENT_SECRET")
      ]
    ]
end

Environment Variables

Set the following environment variables:

# Production credentials
export REA_PROD_CLIENT_ID="your_client_id"
export REA_PROD_CLIENT_SECRET="your_client_secret"

Multiple Environments

You can configure and use multiple environments simultaneously:

config :rea,
  environments: [
    prod: [
      base_url: "https://api.realestate.com.au",
      client_id: System.get_env("REA_PROD_CLIENT_ID"),
      client_secret: System.get_env("REA_PROD_CLIENT_SECRET")
    ],
    test: [
      base_url: "https://api.test.realestate.com.au",
      client_id: System.get_env("REA_TEST_CLIENT_ID"),
      client_secret: System.get_env("REA_TEST_CLIENT_SECRET")
    ]
  ]

# Use them simultaneously in your application
prod_client = Rea.Client.new(environment: :prod)
test_client = Rea.Client.new(environment: :test)

Usage

Creating a Client

Clients are lightweight structs that reference a configured environment. Credentials are stored in the application config, not in the client:

# Create a client for the production environment
client = Rea.Client.new(environment: :prod)

# Create a client for the test environment
client = Rea.Client.new(environment: :test)

# Or use the convenience function for test environment
client = Rea.Client.test()

# You can also override the base URL if needed
client = Rea.Client.new(
  environment: :prod,
  base_url: "https://custom.example.com"
)

Realestate.com.au Prices

Get product prices for properties listed on realestate.com.au:

# Get prices with required parameters
{:ok, prices} = Rea.Client.RealestateProperties.get_prices(client,
  agency_id: "FEXBNP",
  suburb: "Sydney",
  state: "NSW",
  postcode: "2000"
)

# With optional parameters
{:ok, prices} = Rea.Client.RealestateProperties.get_prices(client,
  agency_id: "FEXBNP",
  suburb: "Sydney",
  state: "NSW",
  postcode: "2000",
  section: "rent",           # "buy" (default), "rent", or "sold"
  listing_type: "residential", # "residential" (default), "land", or "rural"
  reupgrade: false,          # default: false
  date: "2024-06-30T10:00:00Z"
)

The response includes product categories:

Realcommercial.com.au Prices

Get product prices for properties listed on realcommercial.com.au:

{:ok, prices} = Rea.Client.CommercialProperties.get_prices(client,
  agency_id: "FEXBNP",
  suburb: "Sydney",
  state: "NSW",
  postcode: "2000"
)

The response includes:

Error Handling

The client returns tuples with {:ok, result} or {:error, reason}:

case Rea.Client.RealestateProperties.get_prices(client, params) do
  {:ok, prices} ->
    # Process prices
    IO.inspect(prices)

  {:error, {:not_found, body}} ->
    # Handle not found - no products available
    IO.puts("No products found: #{body["title"]}")

  {:error, {:unauthorized, body}} ->
    # Handle authentication error
    IO.puts("Authentication failed: #{body["title"]}")

  {:error, {:bad_request, body}} ->
    # Handle invalid parameters
    IO.puts("Invalid request: #{body["detail"]}")

  {:error, reason} ->
    # Handle other errors
    IO.inspect(reason)
end

You can use the Rea.Client.Error module for structured error handling:

case Rea.Client.RealestateProperties.get_prices(client, params) do
  {:ok, prices} ->
    prices

  {:error, reason} ->
    error = Rea.Client.Error.new(reason)
    IO.puts(Rea.Client.Error.message(error))
    IO.puts(Rea.Client.Error.detail(error))
end

Authentication

The client uses a shared token manager architecture for OAuth2 authentication:

  1. Application Startup: When your application starts, a TokenManager GenServer process is started for each configured environment (:prod, :test, etc.)
  2. Automatic Token Acquisition: When you make your first API request, the TokenManager automatically obtains an access token using the OAuth2 client credentials flow
  3. Token Caching: The token is cached in the TokenManager process and shared across all processes in your application
  4. Automatic Refresh: When a token expires, it's automatically refreshed. Only one refresh occurs even with concurrent requests
  5. Race-Condition Free: GenServer's message queue ensures only one token refresh happens at a time, preventing duplicate refresh requests

How It Works

# Multiple processes can safely use the same client
client = Rea.Client.new(environment: :prod)

# These calls all share the same token from TokenManager
Task.async(fn -> Rea.Client.RealestateProperties.get_prices(client, params1) end)
Task.async(fn -> Rea.Client.RealestateProperties.get_prices(client, params2) end)
Task.async(fn -> Rea.Client.CommercialProperties.get_prices(client, params3) end)

# When the token expires, only ONE refresh request is made
# All concurrent calls automatically receive the new token

Benefits

Available Resources

The following resource modules are available:

Development

# Get dependencies
mix deps.get

# Run tests
mix test

# Generate documentation
mix docs

# Format code
mix format

License

MIT