TypeScript SDK

A lightweight TypeScript client for Conduit's governed MLS data access. Copy-paste ready with full type safety.

[i]No npm package required

The client below is self-contained. Copy it into your project and customize as needed. No external dependencies beyond the standard Fetch API.

ConduitClient

typescript
interface QueryOptions {
  method?: string
  toolName?: string
  arguments?: Record<string, unknown>
  resourceUri?: string
}

interface GovernanceMetadata {
  governed: boolean
  antiTraining: boolean
  stateless: boolean
  rateLimitRemaining: number | null
  rateLimitReset: number | null
  billingStatus: string | null
}

interface ConduitResponse<T = unknown> {
  data: T
  governance: GovernanceMetadata
  status: number
}

class ConduitClient {
  private baseUrl: string
  private apiKey: string

  constructor(gatewayUrl: string, apiKey: string) {
    this.baseUrl = gatewayUrl
    this.apiKey = apiKey
  }

  async call<T = unknown>(options: QueryOptions): Promise<ConduitResponse<T>> {
    let body: Record<string, unknown>

    if (options.resourceUri) {
      body = {
        jsonrpc: '2.0',
        method: 'resources/read',
        params: { uri: options.resourceUri },
        id: Date.now(),
      }
    } else {
      body = {
        jsonrpc: '2.0',
        method: options.method ?? 'tools/call',
        params: options.toolName
          ? { name: options.toolName, arguments: options.arguments ?? {} }
          : undefined,
        id: Date.now(),
      }
    }

    const res = await fetch(this.baseUrl, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })

    if (!res.ok) {
      const error = await res.json().catch(() => ({ error: res.statusText }))
      throw new ConduitError(res.status, error.error ?? 'Request failed', error)
    }

    const data = await res.json()

    return {
      data: data as T,
      governance: {
        governed: res.headers.get('X-Conduit-Governed') === 'true',
        antiTraining: res.headers.get('X-Conduit-Anti-Training') === 'true',
        stateless: res.headers.get('X-Conduit-Stateless') === 'true',
        rateLimitRemaining: parseNum(res.headers.get('X-RateLimit-Remaining')),
        rateLimitReset: parseNum(res.headers.get('X-RateLimit-Reset')),
        billingStatus: res.headers.get('X-Conduit-Billing'),
      },
      status: res.status,
    }
  }

  async listTools() {
    return this.call({ method: 'tools/list' })
  }

  async searchProperties(args: Record<string, unknown>) {
    return this.call({ toolName: 'search_properties', arguments: args })
  }

  async readListing(listingUri: string) {
    return this.call({ resourceUri: listingUri })
  }
}

class ConduitError extends Error {
  status: number
  details: unknown

  constructor(status: number, message: string, details: unknown) {
    super(message)
    this.status = status
    this.details = details
  }
}

function parseNum(val: string | null): number | null {
  if (!val) return null
  const n = parseInt(val, 10)
  return isNaN(n) ? null : n
}

Usage examples

List available tools

typescript
const client = new ConduitClient(
  'https://gateway.conduitapi.dev/s/austin-mls/reso-feed',
  'cnd_live_xxxxxxxxxxxxxxxxxxxx'
)

const { data, governance } = await client.listTools()
console.log('Tools:', data)
console.log('Governed:', governance.governed)
typescript
const { data, governance } = await client.searchProperties({
  city: 'Austin',
  propertyType: 'Residential',
  minPrice: 300000,
  maxPrice: 500000,
  limit: 25,
})

console.log('Results:', data)
console.log('Rate limit remaining:', governance.rateLimitRemaining)

// Check anti-training requirement
if (governance.antiTraining) {
  console.log('Data must not be used for model training')
}

Error handling

typescript
try {
  const result = await client.searchProperties({ city: 'Austin' })
} catch (err) {
  if (err instanceof ConduitError) {
    switch (err.status) {
      case 429:
        const retryAfter = (err.details as { retry_after?: number }).retry_after
        console.log(`Rate limited. Retry in ${retryAfter}s`)
        break
      case 403:
        console.log('Access denied — check billing or MLS approval')
        break
      case 502:
        console.log('MLS data feed unreachable — retry with backoff')
        break
      default:
        console.error(`Error ${err.status}: ${err.message}`)
    }
  }
}