ZoomProp

SDK reference

SDK reference

Last updated 5/3/2026

ZoomProp SDK Reference

Version: 0.1.0-alpha | Base URL: https://api.zoomprop.com/v1 | Auth: Clerk session tokens


Table of Contents

  1. Installation
  2. Authentication Setup
  3. Client Initialization
  4. TypeScript Type Definitions
  5. API Method Reference
  6. Pagination Patterns
  7. Rate Limiting
  8. Error Handling
  9. Webhook Handling

Installation

ZoomProp does not yet publish a standalone npm package. All API interaction happens through Next.js route handlers at /api/* within the zoomprop-ai-platform monorepo, or via direct HTTP from external clients using a Clerk session token as a bearer credential.

# Install the platform locally
git clone https://github.com/zoomprop/zp-alpha.git
cd zp-alpha
npm install

# Or consume the API directly with a typed fetch wrapper:
npm install @clerk/clerk-js zod
# Python integration tests use the bundled test client
cd api_tests
pip install -r requirements.txt

Authentication Setup

ZoomProp uses Clerk for all authentication. Every API route is protected by Clerk's auth() helper. External callers must supply a valid Clerk session token in the Authorization header.

Obtaining a Session Token

// In a Clerk-authenticated browser context
import { useAuth } from '@clerk/nextjs';

function useZoomPropToken() {
  const { getToken } = useAuth();
  return () => getToken(); // returns Promise<string | null>
}

Environment Variables

Configure the following variables before initializing any client:

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
NEXT_PUBLIC_ZOOMPROP_API_URL_V1=https://api.zoomprop.com/v1
NEXT_PUBLIC_ZOOMPROP_WEBSOCKET_URL=wss://ws.zoomprop.com
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.eyJ1...
OPENAI_API_KEY=sk-...

Client Initialization

// src/lib/zoomprop-client.ts

export interface ZoomPropClientConfig {
  /** Base URL for internal Next.js API routes. Defaults to '' (same origin). */
  baseUrl?: string;
  /** Clerk session token resolver. Required for external callers. */
  getToken: () => Promise<string | null>;
  /** Request timeout in milliseconds. Defaults to 30000. */
  timeoutMs?: number;
  /** Organization ID to scope requests. Maps to Clerk's active org. */
  organizationId?: string;
}

export class ZoomPropClient {
  private baseUrl: string;
  private getToken: () => Promise<string | null>;
  private timeoutMs: number;
  private organizationId?: string;

  constructor(config: ZoomPropClientConfig) {
    this.baseUrl = config.baseUrl ?? '';
    this.getToken = config.getToken;
    this.timeoutMs = config.timeoutMs ?? 30_000;
    this.organizationId = config.organizationId;
  }

  async fetch<T>(
    path: string,
    init: RequestInit = {}
  ): Promise<T> {
    const token = await this.getToken();
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeoutMs);

    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...(this.organizationId
        ? { 'X-Organization-Id': this.organizationId }
        : {}),
      ...(init.headers as Record<string, string> | undefined),
    };

    try {
      const res = await fetch(`${this.baseUrl}${path}`, {
        ...init,
        headers,
        signal: controller.signal,
      });

      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new ZoomPropAPIError(res.status, body.error ?? res.statusText, body);
      }

      return res.json() as Promise<T>;
    } finally {
      clearTimeout(timer);
    }
  }
}

// Singleton for use inside Next.js server components / route handlers
import { auth } from '@clerk/nextjs/server';

export function createServerClient(): ZoomPropClient {
  return new ZoomPropClient({
    getToken: async () => {
      const { getToken } = await auth();
      return getToken();
    },
  });
}

React hook (client-side)

// src/hooks/use-zoomprop-client.ts
import { useAuth } from '@clerk/nextjs';
import { useMemo } from 'react';
import { ZoomPropClient } from '@/lib/zoomprop-client';

export function useZoomPropClient(): ZoomPropClient {
  const { getToken, orgId } = useAuth();
  return useMemo(
    () =>
      new ZoomPropClient({
        getToken: () => getToken(),
        organizationId: orgId ?? undefined,
      }),
    [getToken, orgId]
  );
}

TypeScript Type Definitions

// ─── Shared primitives ────────────────────────────────────────────────────────

export type ISODateString = string; // "2024-01-15T00:00:00Z"
export type PropertyId = string;
export type ConversationId = string;
export type AlertId = string;
export type FilterId = string;

// ─── Errors ───────────────────────────────────────────────────────────────────

export class ZoomPropAPIError extends Error {
  constructor(
    public readonly status: number,
    message: string,
    public readonly body: unknown
  ) {
    super(message);
    this.name = 'ZoomPropAPIError';
  }
}

// ─── Auth ─────────────────────────────────────────────────────────────────────

export interface SessionResponse {
  userId: string;
  orgId: string | null;
  orgRole: string | null;
  sessionId: string;
}

export interface ActiveOrgResponse {
  organizationId: string;
  organizationName: string;
  role: string;
}

// ─── Pagination ───────────────────────────────────────────────────────────────

export interface PaginationParams {
  page?: number;     // 1-indexed, default 1
  limit?: number;    // default 20, max 100
  cursor?: string;   // opaque cursor for cursor-based pagination
}

export interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  hasMore: boolean;
  nextCursor?: string;
}

// ─── Properties ───────────────────────────────────────────────────────────────

export interface Property {
  id: PropertyId;
  address: string;
  city: string;
  state: string;
  zipCode: string;
  listPrice: number;
  bedrooms: number;
  bathrooms: number;
  squareFeet: number;
  propertyType: 'residential' | 'commercial' | 'multi-family' | 'land';
  status: 'active' | 'pending' | 'sold' | 'off-market';
  latitude: number;
  longitude: number;
  createdAt: ISODateString;
  updatedAt: ISODateString;
}

export interface PropertySearchParams extends PaginationParams {
  query?: string;
  city?: string;
  state?: string;
  zipCode?: string;
  minPrice?: number;
  maxPrice?: number;
  minBeds?: number;
  maxBeds?: number;
  propertyType?: Property['propertyType'];
  status?: Property['status'];
  lat?: number;
  lng?: number;
  radiusMiles?: number;
}

// ─── Cap Rate ─────────────────────────────────────────────────────────────────

export interface CapRateInput {
  constMonthlyRent: number | string;
  constVacancy?: number;       // default 5
  constOperExpenses?: number;  // default 30
  constResEstimate?: number;   // default 5
  constClosingCosts?: number;  // default 2
  listPrice: number;
}

export interface CapRateResult {
  capRate: number;
  annualNOI: number;
  grossRentalIncome: number;
  effectiveGrossIncome: number;
  totalOperatingExpenses: number;
  netCashFlow: number;
}

// ─── AI Chat ──────────────────────────────────────────────────────────────────

export interface ChatMessage {
  role: 'user' | 'assistant' | 'system';
  content: string;
}

export interface ChatRequest {
  messages: ChatMessage[];
  conversationId?: ConversationId;
  propertyId?: PropertyId;
  stream?: boolean;
}

export interface ChatResponse {
  message: ChatMessage;
  conversationId: ConversationId;
  usage?: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
  };
}

// ─── Conversations ────────────────────────────────────────────────────────────

export interface Conversation {
  id: ConversationId;
  title: string;
  messages: ChatMessage[];
  propertyId?: PropertyId;
  createdAt: ISODateString;
  updatedAt: ISODateString;
}

export interface CreateConversationRequest {
  title?: string;
  propertyId?: PropertyId;
  initialMessage?: ChatMessage;
}

export interface GenerateTitleRequest {
  conversationId: ConversationId;
  messages: ChatMessage[];
}

export interface GenerateTitleResponse {
  title: string;
}

// ─── AI Analysis ──────────────────────────────────────────────────────────────

export interface PropertyAnalysisRequest {
  propertyId: PropertyId;
  includeComps?: boolean;
  includeMarketTrends?: boolean;
}

export interface PropertyAnalysisResponse {
  propertyId: PropertyId;
  summary: string;
  investmentScore: number;
  risks: string[];
  opportunities: string[];
  comparables: Property[];
  marketTrends: MarketTrend[];
  generatedAt: ISODateString;
}

export interface CommercialAnalysisRequest {
  propertyId: PropertyId;
  capRateInput?: CapRateInput;
}

export interface CommercialAnalysisResponse {
  propertyId: PropertyId;
  capRate: CapRateResult;
  leaseAnalysis: string;
  tenantProfile: string;
  marketPosition: string;
  recommendation: string;
}

export interface CommercialEstimatesRequest {
  propertyId: PropertyId;
  propertyType: string;
  squareFeet: number;
  location: { lat: number; lng: number };
}

export interface CommercialEstimatesResponse {
  estimatedRent: number;
  rentRange: { min: number; max: number };
  occupancyRate: number;
  capRateRange: { min: number; max: number };
}

export interface MaintenanceAnalysisRequest {
  propertyId: PropertyId;
  propertyAge?: number;
  lastInspectionDate?: ISODateString;
  knownIssues?: string[];
}

export interface MaintenanceAnalysisResponse {
  immediateItems: MaintenanceItem[];
  shortTermItems: MaintenanceItem[];
  longTermItems: MaintenanceItem[];
  estimatedTotalCost: number;
  priorityScore: number;
}

export interface MaintenanceItem {
  description: string;
  estimatedCost: number;
  urgency: 'immediate' | 'short-term' | 'long-term';
  category: string;
}

export interface OfferAnalysisRequest {
  propertyId: PropertyId;
  offerPrice: number;
  listPrice: number;
  comparables?: { address: string; salePrice: number; date: ISODateString }[];
}

export interface OfferAnalysisResponse {
  recommendation: 'accept' | 'counter' | 'reject';
  fairValueEstimate: number;
  confidenceScore: number;
  rationale: string;
  suggestedCounterOffer?: number;
}

export interface PropertyInspectionAnalysisRequest {
  propertyId: PropertyId;
  inspectionNotes: string;
  images?: string[];
}

export interface PropertyInspectionAnalysisResponse {
  findings: InspectionFinding[];
  overallCondition: 'excellent' | 'good' | 'fair' | 'poor';
  recommendedActions: string[];
  estimatedRepairCost: number;
}

export interface InspectionFinding {
  area: string;
  severity: 'critical' | 'moderate' | 'minor';
  description: string;
  estimatedCost: number;
}

export interface IntentRequest {
  query: string;
  context?: Record<string, unknown>;
}

export interface IntentResponse {
  intent: string;
  confidence: number;
  entities: Record<string, string>;
  suggestedAction: string;
}

export interface SuggestTagsRequest {
  propertyId?: PropertyId;
  description?: string;
  propertyType?: string;
}

export interface SuggestTagsResponse {
  tags: string[];
}

export interface AIPersona {
  id: string;
  name: string;
  description: string;
  systemPrompt: string;
  capabilities: string[];
}

export interface PerformanceMonitoringResponse {
  latencyP50Ms: number;
  latencyP99Ms: number;
  tokensPerMinute: number;
  errorRate: number;
  totalRequests: number;
  windowStart: ISODateString;
  windowEnd: ISODateString;
}

// ─── AI Search Templates ──────────────────────────────────────────────────────

export interface AISearchTemplate {
  id: string;
  name: string;
  query: string;
  filters: PropertySearchParams;
  createdAt: ISODateString;
}

export interface CreateAISearchTemplateRequest {
  name: string;
  query: string;
  filters?: PropertySearchParams;
}

// ─── Suggestions ──────────────────────────────────────────────────────────────

export interface SuggestionsRequest {
  query: string;
  context?: 'property' | 'market' | 'investment';
  limit?: number;
}

export interface SuggestionsResponse {
  suggestions: string[];
}

// ─── Analytics ────────────────────────────────────────────────────────────────

export interface MarketTrend {
  period: ISODateString;
  medianPrice: number;
  daysOnMarket: number;
  listingsCount: number;
  soldCount: number;
  pricePerSqft: number;
}

export interface MarketAnalyticsParams {
  region?: string;
  state?: string;
  city?: string;
  zipCode?: string;
  startDate?: ISODateString;
  endDate?: ISODateString;
}

export interface PortfolioAnalyticsResponse {
  totalProperties: number;
  totalValue: number;
  totalEquity: number;
  monthlyIncome: number;
  averageCapRate: number;
  topPerformers: Property[];
  underperformers: Property[];
}

export interface AppreciationDistributionResponse {
  buckets: { range: string; count: number; percentage: number }[];
  median: number;
  mean: number;
  standardDeviation: number;
}

export interface AnalyticsDashboardResponse {
  summary: {
    activeListings: number;
    averageListPrice: number;
    marketTemperature: 'hot' | 'neutral' | 'cold';
  };
  trends: MarketTrend[];
  topMarkets: { name: string; growth: number }[];
}

export interface PropertiesAnalyticsParams extends PaginationParams {
  sortBy?: 'capRate' | 'appreciation' | 'cashFlow' | 'listPrice';
  sortOrder?: 'asc' | 'desc';
  propertyType?: Property['propertyType'];
}

// ─── Alerts ───────────────────────────────────────────────────────────────────

export interface Alert {
  id: AlertId;
  type: string;
  title: string;
  message: string;
  propertyId?: PropertyId;
  read: boolean;
  archived: boolean;
  createdAt: ISODateString;
}

export interface AlertConfigRequest {
  alertType: string;
  enabled: boolean;
  threshold?: number;
  channels: ('email' | 'sms' | 'in-app')[];
  propertyId?: PropertyId;
}

export interface AlertConfigResponse {
  id: string;
  alertType: string;
  enabled: boolean;
  channels: string[];
  createdAt: ISODateString;
}

export interface AlertPreferences {
  emailEnabled: boolean;
  smsEnabled: boolean;
  inAppEnabled: boolean;
  digestFrequency: 'immediate' | 'daily' | 'weekly';
  quietHoursStart?: string; // "22:00"
  quietHoursEnd?: string;   // "08:00"
}

export interface AlertHistoryParams extends PaginationParams {
  startDate?: ISODateString;
  endDate?: ISODateString;
  type?: string;
  read?: boolean;
  archived?: boolean;
}

export interface TriggerAlertRequest {
  alertType: string;
  propertyId?: PropertyId;
  payload?: Record<string, unknown>;
}

export interface TestAlertRequest {
  alertType: string;
  channel: 'email' | 'sms' | 'in-app';
}

export interface AlertUsersResponse {
  users: {
    id: string;
    email: string;
    alertsEnabled: boolean;
    lastAlertAt?: ISODateString;
  }[];
}

export interface ArchiveMessageRequest {
  messageIds: string[];
}

// ─── Alert Filters ────────────────────────────────────────────────────────────

export interface AlertFilter {
  id: FilterId;
  name: string;
  conditions: AlertFilterCondition[];
  createdAt: ISODateString;
}

export interface AlertFilterCondition {
  field: string;
  operator: 'eq' | 'neq' | 'gt' | 'lt' | 'contains';
  value: string | number | boolean;
}

export interface CreateAlertFilterRequest {
  name: string;
  conditions: AlertFilterCondition[];
}

export interface AlertFilterAssignment {
  filterId: FilterId;
  userId?: string;
  orgId?: string;
  propertyId?: PropertyId;
}

// ─── Automation ───────────────────────────────────────────────────────────────

export interface AutomationAnalyticsResponse {
  totalRuns: number;
  successRate: number;
  averageDurationMs: number;
  failureReasons: { reason: string; count: number }[];
  periodStart: ISODateString;
  periodEnd: ISODateString;
}

export interface BoardConfig {
  id: string;
  columns: BoardColumn[];
  automations: BoardAutomation[];
}

export interface BoardColumn {
  id: string;
  name: string;
  order: number;
  cardLimit?: number;
}

export interface BoardAutomation {
  id: string;
  trigger: string;
  action: string;
  enabled: boolean;
}

// ─── Articles ─────────────────────────────────────────────────────────────────

export interface Article {
  id: string;
  title: string;
  slug: string;
  excerpt: string;
  content: string;
  author: string;
  tags: string[];
  publishedAt: ISODateString;
}

export interface ArticlesParams extends PaginationParams {
  tag?: string;
  query?: string;
}

// ─── Property Investigation ───────────────────────────────────────────────────

export interface PropertyInvestigationRequest {
  propertyId: PropertyId;
  investigationType: 'full' | 'quick' | 'financial';
  includeNeighborhood?: boolean;
  includeSchools?: boolean;
  includeWalkScore?: boolean;
}

export interface PropertyInvestigationResponse {
  propertyId: PropertyId;
  financialAnalysis: CapRateResult;
  neighborhoodScore?: number;
  schoolRating?: number;
  walkScore?: number;
  investmentGrade: 'A' | 'B' | 'C' | 'D' | 'F';
  aiSummary: string;
  generatedAt: ISODateString;
}

API Method Reference

The following methods are organized by domain. All methods are implemented as typed wrappers over ZoomPropClient.fetch.


Auth

GET /api/auth/session

Retrieves the current Clerk session details.

Method signature

async getSession(): Promise<SessionResponse>

Parameters: None

Return type: SessionResponse

Example

const client = new ZoomPropClient({ getToken });
const session = await client.fetch<SessionResponse>('/api/auth/session');
console.log(session.userId); // "user_2abc..."

Error handling

StatusMeaning
401No valid Clerk session
403Session exists but organization access denied

GET /api/auth/active-org

Returns the currently active Clerk organization for the authenticated user.

Method signature

async getActiveOrg(): Promise<ActiveOrgResponse>

Parameters: None

Return type: ActiveOrgResponse

Example

const org = await client.fetch<ActiveOrgResponse>('/api/auth/active-org');
console.log(org.organizationId); // "org_2xyz..."

GET /api/auth/test

Health-check endpoint that verifies the Clerk auth middleware is functioning. Returns 200 OK with a success payload if the session is valid.

Method signature

async testAuth(): Promise<{ ok: boolean; userId: string }>

Example

const result = await client.fetch<{ ok: boolean; userId: string }>('/api/auth/test');

AI Chat & Conversations

POST /api/ai/chat

Sends a chat message and receives an AI-generated response. Supports both streaming and non-streaming modes. The underlying route uses @ai-sdk/openai and LangChain.

Method signature

async chat(request: ChatRequest): Promise<ChatResponse>

Parameters

ParameterTypeRequiredDescription
messagesChatMessage[]YesFull conversation history
conversationIdConversationIdNoLinks message to a stored conversation
propertyIdPropertyIdNoScopes the AI context to a specific property
streambooleanNoDefault false. Set true for SSE streaming

Return type: ChatResponse

Example — non-streaming

const response = await client.fetch<ChatResponse>('/api/ai/chat', {
  method: 'POST',
  body: JSON.stringify({
    messages: [{ role: 'user', content: 'What is the cap rate for this property?' }],
    propertyId: 'prop_abc123',
    conversationId: 'conv_xyz789',
  }),
});

console.log(response.message.content);

Example — streaming (browser only)

const token = await getToken();
const res = await fetch('/api/ai/chat', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ messages, stream: true }),
});

const reader = res.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(decoder.decode(value));
}

Error handling

StatusMeaning
400Malformed messages array
401Missing or expired Clerk token
429Rate limit exceeded (see Rate Limiting)
500OpenAI upstream error

GET /api/ai/conversations

Lists all conversations for the authenticated user.

Method signature

async listConversations(
  params?: PaginationParams
): Promise<PaginatedResponse<Conversation>>

Parameters

ParameterTypeRequiredDescription
pagenumberNoPage number, default 1
limitnumberNoItems per page, default 20

Return type: PaginatedResponse<Conversation>

Example

const params = new URLSearchParams({ page: '1', limit: '20' });
const conversations = await client.fetch<PaginatedResponse<Conversation>>(
  `/api/ai/conversations?${params}`
);

POST /api/ai/conversations

Creates a new conversation record.

Method signature

async createConversation(
  request: CreateConversationRequest
): Promise<Conversation>

Parameters

ParameterTypeRequiredDescription
titlestringNoAuto-generated if omitted
propertyIdPropertyIdNoAssociates the conversation with a property
initialMessageChatMessageNoFirst message to seed the conversation

Return type: Conversation

Example

const conversation = await client.fetch<Conversation>('/api/ai/conversations', {
  method: 'POST',
  body: JSON.stringify({
    propertyId: 'prop_abc123',
    title: 'Investment analysis for 123 Main St',
  }),
});

GET /api/ai/conversations/:id

Retrieves a single conversation by ID including its full message history.

Method signature

async getConversation(id: ConversationId): Promise<Conversation>

Parameters

ParameterTypeRequiredDescription
idConversationIdYesConversation identifier (path parameter)

Return type: Conversation

Example

const conversation = await client.fetch<Conversation>(
  `/api/ai/conversations/conv_xyz789`
);

Error handling

StatusMeaning
404Conversation not found or does not belong to authenticated user

DELETE /api/ai/conversations/:id

Deletes a conversation and its message history.

Method signature

async deleteConversation(id: ConversationId): Promise<{ deleted: boolean }>

Example

const result = await client.fetch<{ deleted: boolean }>(
  `/api/ai/conversations/conv_xyz789`,
  { method: 'DELETE' }
);

POST /api/ai/generate-title

Generates a human-readable title for an existing conversation using AI.

Method signature

async generateConversationTitle(
  request: GenerateTitleRequest
): Promise<GenerateTitleResponse>

Parameters

ParameterTypeRequiredDescription
conversationIdConversationIdYesTarget conversation
messagesChatMessage[]YesMessages to base the title on

Return type: GenerateTitleResponse

Example

const { title } = await client.fetch<GenerateTitleResponse>(
  '/api/ai/generate-title',
  {
    method: 'POST',
    body: JSON.stringify({
      conversationId: 'conv_xyz789',
      messages: conversation.messages,
    }),
  }
);

POST /api/ai/intent

Classifies the user's intent from a natural language query. Used by the chat shell to route queries to the correct analysis pipeline.

Method signature

async detectIntent(request: IntentRequest): Promise<IntentResponse>

Parameters

ParameterTypeRequiredDescription
querystringYesRaw user input
contextRecord<string, unknown>NoAdditional context for disambiguation

Return type: IntentResponse

Example

const intent = await client.fetch<IntentResponse>('/api/ai/intent', {
  method: 'POST',
  body: JSON.stringify({ query: 'Show me cap rates in Austin above 6%' }),
});

console.log(intent.intent);     // "property_search"
console.log(intent.confidence); // 0.94

GET /api/ai/suggestions

Returns AI-powered query suggestions based on user input, useful for powering autocomplete in the search bar.

Method signature

async getSuggestions(
  request: SuggestionsRequest
): Promise<SuggestionsResponse>

Parameters

ParameterTypeRequiredDescription
querystringYesPartial user input
context'property' | 'market' | 'investment'NoNarrows suggestion domain
limitnumberNoMax suggestions to return, default 5

Return type: SuggestionsResponse

Example

const params = new URLSearchParams({ query: 'multi-family in', limit: '5' });
const { suggestions } = await client.fetch<SuggestionsResponse>(
  `/api/ai/suggestions?${params}`
);

GET /api/ai/personas

Lists available AI advisor personas (e.g., "Investment Analyst", "Property Manager").

Method signature

async getPersonas(): Promise<AIPersona[]>

Return type: AIPersona[]

Example

const personas = await client.fetch<AIPersona[]>('/api/ai/personas');

GET /api/ai/performance-monitoring

Returns real-time AI performance metrics for the authenticated organization. Intended for administrators.

Method signature

async getAIPerformanceMetrics(): Promise<PerformanceMonitoringResponse>

Return type: PerformanceMonitoringResponse

Example

const metrics = await client.fetch<PerformanceMonitoringResponse>(
  '/api/ai/performance-monitoring'
);
console.log(`P99 latency: ${