@darksquarebishop/rets-client-ts
v0.1.0
Published
Modern TypeScript RETS (Real Estate Transaction Standard) client
Maintainers
Readme
rets-client-ts
Modern TypeScript client for RETS (Real Estate Transaction Standard) — the legacy XML-over-HTTP protocol used by MLS systems to expose listing, roster, and media data.
RETS is being superseded by RESO Web APIs. This client targets existing RETS endpoints (Spark/flexmls, GSMLS, BMLS, and similar).
Requirements: Node.js 20+, ESM only.
Install
npm install @darksquarebishop/rets-client-tsQuick start
import { createClient } from "@darksquarebishop/rets-client-ts";
const client = await createClient({
loginUrl: "https://rets.example.com/login",
username: "user",
password: "pass",
userAgent: "my-app/1.0",
});
await client.login();
const { rows, count, maxRowsExceeded } = await client.search(
"Property",
"Listing",
"(ModificationTimestamp=2024-01-01+)",
{ limit: 100, offset: 1, format: "COMPACT-DECODED" },
);
console.log(`Found ${rows.length} listings (total: ${count})`);
await client.logout();Auto-logout with await using
import { createClient } from "@darksquarebishop/rets-client-ts";
const client = await createClient({
/* ... */
});
await client.login();
await using _session = client;
// client[Symbol.asyncDispose]() runs logout() when scope exits
const resources = await client.metadata.getResources();Provider profiles
Built-in presets for MLS-specific transport behavior (RETS version, session mode, retry):
import { createClient, providers } from "@darksquarebishop/rets-client-ts";
// Garden State MLS — RETS/1.8, UA auth, per-request sessions
const gsmls = await createClient({
...providers.gsmls,
loginUrl: process.env.GSMLS_URL!,
username: process.env.GSMLS_USER!,
password: process.env.GSMLS_PASSWORD!,
userAgent: process.env.GSMLS_USER_AGENT!,
userAgentPassword: process.env.GSMLS_USER_AGENT_PASSWORD!,
});
// Brooklyn MLS — RETS/1.7.2, persistent session, no UA auth
const bmls = await createClient({
...providers.bmls,
loginUrl: process.env.BROOKLYNMLS_URL!,
username: process.env.BROOKLYNMLS_USER!,
password: process.env.BROOKLYNMLS_PASSWORD!,
});Profiles set transport/session defaults only. Resource names, class tags, DMQL strings, and pagination limits are caller concerns.
Search
Both COMPACT and COMPACT-DECODED formats are supported (default: COMPACT-DECODED).
// Buffered — returns all rows in memory
const result = await client.search("Property", "RES", "(LSTNGSYSID=123+)", {
limit: 200,
offset: 1,
select: "LSTNGSYSID,LISTPRICE",
});
// Streaming — yields rows one at a time
for await (const row of await client.searchStream("Property", "RES", query)) {
await db.upsert(row);
}
// Auto-pagination — advances offset on maxRowsExceeded
for await (const row of client.searchAll("Property", "RES", query, { limit: 200 })) {
await db.upsert(row);
}Metadata
const resources = await client.metadata.getResources();
const classes = await client.metadata.getClass("Property");
const tables = await client.metadata.getTable("Property", "RES");
const system = await client.metadata.getSystem();Objects (photos / media)
Returns an async iterator of parts (binary data, URL location, or error):
// Specific objects
for await (const obj of client.getObjects("Property", "Photo", { "123": "*" })) {
if (obj.data) await fs.writeFile(`${obj.headerInfo.contentId}.jpg`, obj.data);
if (obj.location) console.log(obj.location);
if (obj.error) console.warn(obj.error);
}
// All photos for a listing
for await (const obj of client.getAllObjects("Property", "LargePhoto", "12345")) {
/* ... */
}
// Preferred photo only
for await (const obj of client.getPreferredObjects("Property", "LargePhoto", "12345")) {
/* ... */
}Some MLSs deliver photos as CDN URLs via search (Media/IMG rows with an IMAGEURL field) rather than binary getObject — use client.search() for those.
Authentication
Two independent layers:
- HTTP auth — Basic or Digest (
qop=auth, MD5). Handled automatically via challenge/response. RETS-UA-Authorization— application-level MD5 header required by some MLSs (e.g. GSMLS). SetuserAgent+userAgentPasswordin config; computed per request from the session cookie.
Logging
Structured logging via pino. Pass a custom logger or silence:
import pino from "pino";
const client = await createClient({
// ...
logger: pino({ level: "debug" }),
// or: logger: false
});Credentials are redacted automatically (authorization, password, RETS-UA-Authorization, cookies).
Set RETS_LOG_LEVEL=debug in the environment to use the default logger at that level.
Errors
| Class | When |
| --------------------- | ------------------------------------- |
| RetsServerError | Non-200 HTTP response |
| RetsReplyError | RETS ReplyCode != 0 |
| RetsParseError | XML/multipart parsing failure |
| RetsParamError | Invalid arguments or unsupported auth |
| RetsPermissionError | Login OK but missing capability URLs |
All extend RetsError with cause, retsMethod, and requestId context.
Configuration
interface ClientConfig {
loginUrl: string;
username: string;
password: string;
userAgent?: string;
userAgentPassword?: string; // GSMLS UA auth
version?: string; // default "RETS/1.7.2"
method?: "GET" | "POST"; // default "GET"
timeout?: number;
proxyUrl?: string;
sessionMode?: "persistent" | "per-request"; // default "persistent"
logger?: pino.Logger | false;
retry?: GotRetryOptions; // default off
truncatedXmlRetry?: {
// default off; GSMLS profile sets attempts: 6
attempts: number;
backoffMs: number;
};
}Exports
// Client
createClient, RetsClient
// Provider presets
providers, gsmls, bmls
// Errors
RetsError, RetsServerError, RetsReplyError, RetsParseError,
RetsParamError, RetsPermissionError, ensureRetsError, isTruncatedXmlError
// Utilities
applyDefaults, computeUaAuth, deriveCredentialKey, normalizeUrl
getReplyTag, isSuccessReplyCode, REPLY_TAG_MAP
// Types
ClientConfig, SearchOptions, SearchResult, SearchFormat, ObjectPart,
MetadataResult, SystemMetadata, SessionMode, ProviderProfile, /* ... */Contributing
See CONTRIBUTING.md. PRs welcome; please add a changeset for user-facing changes.
License
MIT © Matt Cassara
