Skip to main content

Installation

npm install foxreach
Requirements: Node.js 18+ (uses native fetch). Zero runtime dependencies.

Quick Start

import { FoxReach } from "foxreach";

const client = new FoxReach({ apiKey: "otr_your_api_key" });

// Create a lead
const lead = await client.leads.create({
  email: "[email protected]",
  firstName: "Jane",
  lastName: "Smith",
  company: "Acme Corp",
});
console.log(`Created: ${lead.id}`);

// List leads
const page = await client.leads.list({ search: "jane", pageSize: 25 });
for (const lead of page) {
  console.log(`${lead.email}${lead.company}`);
}

// Check pagination
console.log(`Page ${page.meta.page}/${page.meta.totalPages}, ${page.meta.total} total`);

Configuration

const client = new FoxReach({
  apiKey: "otr_...",                                  // required
  baseUrl: "https://api.foxreach.io/api/v1",          // default
  timeout: 30000,                                      // ms, default
  maxRetries: 3,                                       // retries on 429, default
  fetch: customFetch,                                  // optional, for edge runtimes
});
You can also use environment variables:
export FOXREACH_API_KEY=otr_your_api_key
const client = new FoxReach({ apiKey: process.env.FOXREACH_API_KEY! });

Resources

Leads

// List with filters
const page = await client.leads.list({ page: 1, pageSize: 50, search: "acme", status: "active" });

// Get by ID
const lead = await client.leads.get("cld_abc123");

// Create
const newLead = await client.leads.create({
  email: "[email protected]",
  firstName: "Jane",
  company: "Acme Corp",
  tags: ["enterprise"],
});

// Update
await client.leads.update("cld_abc123", { company: "New Corp" });

// Delete
await client.leads.delete("cld_abc123");

Campaigns

// List
const campaigns = await client.campaigns.list({ status: "active" });

// Create (starts in draft)
const campaign = await client.campaigns.create({
  name: "Q1 Outreach",
  timezone: "America/New_York",
  dailyLimit: 50,
});

// Update
await client.campaigns.update("cmp_xyz789", { dailyLimit: 100 });

// Lifecycle
await client.campaigns.start("cmp_xyz789");
await client.campaigns.pause("cmp_xyz789");

// Add leads and accounts
await client.campaigns.addLeads("cmp_xyz789", ["cld_1", "cld_2"]);
await client.campaigns.addAccounts("cmp_xyz789", ["acc_1"]);

// Delete (must be draft)
await client.campaigns.delete("cmp_xyz789");

Sequences

// List steps for a campaign
const steps = await client.campaigns.sequences.list("cmp_xyz789");

// Add a step
const step = await client.campaigns.sequences.create("cmp_xyz789", {
  subject: "Quick question about {{company}}",
  body: "Hi {{firstName}}, ...",
  delayDays: 0,
});

// Update
await client.campaigns.sequences.update("cmp_xyz789", "seq_1", { delayDays: 3 });

// Delete
await client.campaigns.sequences.delete("cmp_xyz789", "seq_1");

Templates

const page = await client.templates.list();
const template = await client.templates.get("tpl_abc123");
const created = await client.templates.create({ name: "Cold Intro", body: "Hi {{firstName}}" });
await client.templates.update("tpl_abc123", { name: "Updated name" });
await client.templates.delete("tpl_abc123");

Email Accounts

const accounts = await client.emailAccounts.list();
const account = await client.emailAccounts.get("acc_abc123");
await client.emailAccounts.delete("acc_abc123");

Inbox

// List unread threads
const threads = await client.inbox.listThreads({ isRead: false, category: "interested" });

// Get a thread
const thread = await client.inbox.get("rpl_abc123");

// Mark as read and categorize
await client.inbox.update("rpl_abc123", { isRead: true, category: "interested" });

Analytics

// Dashboard overview
const overview = await client.analytics.overview();
console.log(`Leads: ${overview.totalLeads}, Reply rate: ${overview.replyRate}%`);

// Campaign stats with daily breakdown
const stats = await client.analytics.campaign("cmp_xyz789");
console.log(`Sent: ${stats.sent}, Replied: ${stats.replied}`);
for (const day of stats.dailyStats) {
  console.log(`  ${day.date}: ${day.sent} sent, ${day.replied} replied`);
}

Pagination

List endpoints return PaginatedResponse objects:
const page = await client.leads.list({ page: 1, pageSize: 50 });

// Iterate current page
for (const lead of page) {
  console.log(lead.email);
}

// Check pagination metadata
console.log(`Page ${page.meta.page}/${page.meta.totalPages}`);

// Get next page
if (page.hasNextPage()) {
  const nextPage = await page.nextPage();
}

// Auto-paginate through ALL results (async iterator)
for await (const lead of (await client.leads.list({ pageSize: 100 })).autoPagingIter()) {
  console.log(lead.email);
}

Error Handling

import {
  FoxReach,
  FoxReachError,
  AuthenticationError,
  NotFoundError,
  RateLimitError,
  ValidationError,
  BadRequestError,
  ServerError,
} from "foxreach";

try {
  const lead = await client.leads.get("cld_nonexistent");
} catch (err) {
  if (err instanceof NotFoundError) {
    console.log("Lead not found");
  } else if (err instanceof AuthenticationError) {
    console.log("Invalid API key");
  } else if (err instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${err.retryAfter}s`);
  } else if (err instanceof ValidationError) {
    console.log(`Validation: ${err.message}`);
  } else if (err instanceof ServerError) {
    console.log(`Server error (${err.statusCode})`);
  } else if (err instanceof FoxReachError) {
    console.log(`API error: ${err.message}`);
  }
}
The SDK automatically retries on 429 Too Many Requests responses (up to maxRetries times) with the delay specified in the Retry-After header.

Type Safety

The SDK provides full TypeScript types for all resources:
import type { Lead, Campaign, Template, EmailAccount, Thread, OverviewStats } from "foxreach";

// All responses are fully typed
const lead: Lead = await client.leads.get("cld_...");
lead.email;       // string
lead.company;     // string | null
lead.tags;        // string[]
lead.createdAt;   // string | null (ISO 8601)

Source Code