code icon Code

List Instantly Campaigns

Fetch all campaigns from Instantly with status, lead counts, and sequence info

Source Code

import fs from "fs";
import path from "path";

const [outputPath] = process.argv.slice(2);

if (!outputPath) {
  console.error("Usage: outreach.instantly.list <outputPath>");
  process.exit(1);
}

const API_BASE = "https://api.instantly.ai/api/v2";
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;

/**
 * Sleep for specified milliseconds
 */
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Fetch with retry logic for transient failures
 */
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const res = await fetch(url, options);

      if (res.ok) {
        return res;
      }

      // Don't retry client errors (4xx)
      if (res.status >= 400 && res.status < 500) {
        return res;
      }

      // Server error - retry with backoff
      if (attempt < retries) {
        const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1);
        console.log(`  Retry ${attempt}/${retries} after ${delay}ms (status: ${res.status})`);
        await sleep(delay);
      } else {
        return res;
      }
    } catch (error) {
      if (attempt < retries) {
        const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1);
        console.log(`  Retry ${attempt}/${retries} after ${delay}ms (error: ${error.message})`);
        await sleep(delay);
      } else {
        throw error;
      }
    }
  }
}

/**
 * Parse API error response and provide actionable guidance
 */
async function handleApiError(res, context) {
  const text = await res.text();

  if (res.status === 401 || res.status === 403) {
    throw new Error(
      `Authentication failed (${res.status}). ` +
      `Check that your Instantly connection is configured correctly and the API key has the required scopes.`
    );
  }

  if (res.status === 429) {
    throw new Error(
      `Rate limit exceeded (${res.status}). ` +
      `Instantly API is throttling requests. Wait a moment and try again.`
    );
  }

  if (res.status >= 500) {
    throw new Error(
      `Instantly API server error (${res.status}) while ${context}. ` +
      `This may be a temporary issue. If it persists, check Instantly's status page.`
    );
  }

  throw new Error(`Instantly API failed while ${context}: ${res.status} - ${text.substring(0, 200)}`);
}

/**
 * Fetch all campaigns from Instantly API v2 with cursor pagination
 */
async function fetchAllCampaigns() {
  const allCampaigns = [];
  let cursor = null;
  const limit = 50;

  do {
    let url = `${API_BASE}/campaigns?limit=${limit}`;
    if (cursor) {
      url += `&starting_after=${cursor}`;
    }

    const res = await fetchWithRetry(url, {
      method: "GET",
      headers: {
        "Authorization": "Bearer PLACEHOLDER_TOKEN",
        "Content-Type": "application/json",
      },
    });

    if (!res.ok) {
      await handleApiError(res, "fetching campaigns");
    }

    const data = await res.json();
    const campaigns = data.items || data || [];
    allCampaigns.push(...campaigns);

    // v2 uses next_starting_after for cursor pagination
    cursor = data.next_starting_after || null;

    if (campaigns.length > 0) {
      console.log(`  Fetched ${allCampaigns.length} campaigns...`);
    }
  } while (cursor);

  return allCampaigns;
}

/**
 * Get campaign details including sequence (v2 endpoint)
 */
async function getCampaignDetails(campaignId) {
  const url = `${API_BASE}/campaigns/${campaignId}`;

  const res = await fetchWithRetry(url, {
    method: "GET",
    headers: {
      "Authorization": "Bearer PLACEHOLDER_TOKEN",
      "Content-Type": "application/json",
    },
  });

  if (!res.ok) {
    // Don't throw on individual campaign errors, just return null
    console.log(`  Warning: Could not fetch details for campaign ${campaignId}`);
    return null;
  }

  return await res.json();
}

try {
  console.log("Fetching campaigns from Instantly...");

  const campaigns = await fetchAllCampaigns();

  console.log(`Found ${campaigns.length} campaigns`);

  if (campaigns.length === 0) {
    // Write empty result
    const output = {
      fetched_at: new Date().toISOString(),
      total_campaigns: 0,
      campaigns: [],
    };

    const outputDir = path.dirname(outputPath);
    if (outputDir && outputDir !== ".") {
      fs.mkdirSync(outputDir, { recursive: true });
    }

    fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
    console.log(`\nāœ“ No campaigns found. Empty result written to: ${outputPath}`);
    console.log(JSON.stringify({ success: true, outputPath, campaignCount: 0 }));
    process.exit(0);
  }

  // Fetch details for each campaign (parallel with limit)
  const CONCURRENCY = 5;
  const detailedCampaigns = [];

  for (let i = 0; i < campaigns.length; i += CONCURRENCY) {
    const batch = campaigns.slice(i, i + CONCURRENCY);
    const details = await Promise.all(
      batch.map(async (campaign) => {
        const detail = await getCampaignDetails(campaign.id);
        return {
          id: campaign.id,
          name: campaign.name,
          status: campaign.status || detail?.status || "unknown",
          lead_count: detail?.lead_count || campaign.lead_count || 0,
          sent_count: detail?.sent_count || campaign.sent_count || 0,
          reply_count: detail?.reply_count || campaign.reply_count || 0,
          sequence_count: detail?.sequences?.length || campaign.sequences?.length || 0,
          created_at: campaign.created_at || detail?.created_at,
        };
      })
    );
    detailedCampaigns.push(...details);
    console.log(`  Processed ${Math.min(i + CONCURRENCY, campaigns.length)}/${campaigns.length}...`);
  }

  // Sort by created date, newest first
  detailedCampaigns.sort((a, b) =>
    new Date(b.created_at || 0) - new Date(a.created_at || 0)
  );

  // Ensure output directory exists
  const outputDir = path.dirname(outputPath);
  if (outputDir && outputDir !== ".") {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  // Write results
  const output = {
    fetched_at: new Date().toISOString(),
    total_campaigns: detailedCampaigns.length,
    campaigns: detailedCampaigns,
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\nāœ“ Campaign data written to: ${outputPath}`);
  console.log(`  Total: ${detailedCampaigns.length} campaigns`);

  // Summary by status
  const byStatus = {};
  for (const c of detailedCampaigns) {
    byStatus[c.status] = (byStatus[c.status] || 0) + 1;
  }
  for (const [status, count] of Object.entries(byStatus)) {
    console.log(`  ${status}: ${count}`);
  }

  console.log(JSON.stringify({
    success: true,
    outputPath,
    campaignCount: detailedCampaigns.length,
    byStatus,
  }));
} catch (error) {
  console.error("Failed:", error.message);
  throw error;
}