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;
}