code icon Code

Create Instantly Campaign

Create a new campaign in Instantly with email sequences

Source Code

const [campaignName, emailAccountId, sequencesJson] = process.argv.slice(2);

if (!campaignName || !emailAccountId || !sequencesJson) {
  console.error("Usage: outreach.instantly.create <campaignName> <emailAccountId> <sequencesJson>");
  console.error("  sequencesJson: '[{\"subject\":\"...\",\"body\":\"...\",\"delay_days\":0}]'");
  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)}`);
}

/**
 * Parse sequences JSON safely
 */
function parseSequences(json) {
  try {
    const sequences = JSON.parse(json);
    if (!Array.isArray(sequences)) {
      throw new Error("Sequences must be an array");
    }

    // Validate sequence structure
    return sequences.map((seq, idx) => {
      const delay = seq.delay_days !== undefined ? seq.delay_days : (idx === 0 ? 0 : 3);

      // First email should have delay 0
      if (idx === 0 && delay !== 0) {
        console.log("  Note: Setting first email delay to 0 (sends immediately when leads added)");
      }

      // Subsequent emails should have delay >= 1
      if (idx > 0 && delay < 1) {
        console.log(`  Note: Email ${idx + 1} delay adjusted to 1 day minimum`);
      }

      return {
        subject: seq.subject || `Email ${idx + 1}`,
        body: seq.body || "",
        delay: idx === 0 ? 0 : Math.max(1, delay),
      };
    });
  } catch (e) {
    throw new Error(`Invalid sequences JSON: ${e.message}`);
  }
}

/**
 * Create campaign via Instantly API v2
 */
async function createCampaign(name, emailAccount, sequences) {
  const url = `${API_BASE}/campaigns`;

  const payload = {
    name: name,
    email_account: emailAccount,
    sequences: sequences.map((seq, idx) => ({
      step: idx + 1,
      subject: seq.subject,
      body: seq.body,
      delay: seq.delay,
    })),
  };

  const res = await fetchWithRetry(url, {
    method: "POST",
    headers: {
      "Authorization": "Bearer PLACEHOLDER_TOKEN",
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });

  if (!res.ok) {
    await handleApiError(res, "creating campaign");
  }

  return await res.json();
}

/**
 * List email accounts to help user pick one (v2 endpoint)
 */
async function listEmailAccounts() {
  const url = `${API_BASE}/email-accounts?limit=100`;

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

  if (!res.ok) {
    console.log("  Warning: Could not fetch email accounts list");
    return [];
  }

  const data = await res.json();
  // v2 returns items array
  return data.items || data || [];
}

try {
  console.log(`Creating campaign: "${campaignName}"`);

  // Parse and validate sequences
  const sequences = parseSequences(sequencesJson);
  console.log(`  Email sequence: ${sequences.length} emails`);

  // Validate minimum sequence requirements
  if (sequences.length === 0) {
    throw new Error("Campaign must have at least one email in the sequence");
  }

  for (let i = 0; i < sequences.length; i++) {
    const seq = sequences[i];

    if (!seq.subject || seq.subject.trim() === "") {
      throw new Error(`Email ${i + 1} is missing a subject line`);
    }

    if (!seq.body || seq.body.trim() === "") {
      throw new Error(`Email ${i + 1} is missing body content`);
    }

    const delay = i === 0 ? "immediately" : `after ${seq.delay} days`;
    console.log(`  ${i + 1}. "${seq.subject}" (${delay})`);
  }

  // Create the campaign
  const result = await createCampaign(campaignName, emailAccountId, sequences);

  console.log(`\nāœ“ Campaign created successfully`);
  console.log(`  ID: ${result.id || "unknown"}`);
  console.log(`  Name: ${campaignName}`);
  console.log(`  Emails: ${sequences.length}`);
  console.log(`  Status: draft (add leads to activate)`);

  console.log(JSON.stringify({
    success: true,
    campaignId: result.id,
    campaignName: campaignName,
    sequenceCount: sequences.length,
    status: "draft",
  }));
} catch (error) {
  console.error("Failed:", error.message);
  throw error;
}