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
- Installation
- Authentication Setup
- Client Initialization
- TypeScript Type Definitions
- API Method Reference
- Pagination Patterns
- Rate Limiting
- Error Handling
- 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
| Status | Meaning |
|---|---|
| 401 | No valid Clerk session |
| 403 | Session 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
| Parameter | Type | Required | Description |
|---|---|---|---|
messages | ChatMessage[] | Yes | Full conversation history |
conversationId | ConversationId | No | Links message to a stored conversation |
propertyId | PropertyId | No | Scopes the AI context to a specific property |
stream | boolean | No | Default 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
| Status | Meaning |
|---|---|
| 400 | Malformed messages array |
| 401 | Missing or expired Clerk token |
| 429 | Rate limit exceeded (see Rate Limiting) |
| 500 | OpenAI upstream error |
GET /api/ai/conversations
Lists all conversations for the authenticated user.
Method signature
async listConversations(
params?: PaginationParams
): Promise<PaginatedResponse<Conversation>>
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page | number | No | Page number, default 1 |
limit | number | No | Items 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
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | No | Auto-generated if omitted |
propertyId | PropertyId | No | Associates the conversation with a property |
initialMessage | ChatMessage | No | First 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | ConversationId | Yes | Conversation identifier (path parameter) |
Return type: Conversation
Example
const conversation = await client.fetch<Conversation>(
`/api/ai/conversations/conv_xyz789`
);
Error handling
| Status | Meaning |
|---|---|
| 404 | Conversation 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
| Parameter | Type | Required | Description |
|---|---|---|---|
conversationId | ConversationId | Yes | Target conversation |
messages | ChatMessage[] | Yes | Messages 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
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Raw user input |
context | Record<string, unknown> | No | Additional 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
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Partial user input |
context | 'property' | 'market' | 'investment' | No | Narrows suggestion domain |
limit | number | No | Max 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: ${