Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mint.skeptrune.com/llms.txt

Use this file to discover all available pages before exploring further.

When we launched new community Discord and Slack bots at Mintlify, we needed to do some customer marketing to let users with large communities know about them. The goal was straightforward: send an email sequence to the 3–10 people from each organization who could get value from the feature. Once one person from an org enables the bot, the task is complete — there’s no reason to keep emailing everyone else from the same company. I thought any of the email marketing tools out there would handle this trivially. They don’t. Every tool available handles campaigns at the user level, not the organization level. None of them have the concept of “stop emailing this group when any member takes action.” They’re all built for individual drip campaigns, not org-level outreach.

Defining the goal

The problem is specific but common in B2B:
  • You have multiple contacts per account — typically 3–10 people per org.
  • You’re marketing a feature where one conversion per org is enough.
  • Once anyone from the org takes action (replies, signs up, enables the feature), you want to stop the sequence for everyone else from that org.
  • You don’t want to spam remaining contacts after the task is already done.
None of the mainstream tools — Resend, Loops, or others — support this natively. They treat every contact as an individual and have no concept of group-level stopping conditions.

Tool selection

Claude Code has made me quite lazy insofar as I try to get everything done using it instead of doing things manually. My number one criteria when picking a tool to solve this was a large API surface that Claude could work with effectively. Instantly was the final selection given its API was the most robust and well documented. It gave Claude full access to campaigns, leads, and sequences, while also being capable of sending webhooks on reply and unsubscribe events.
Kind of a random aside, but JAMstack architecture patterns are probably going to make a comeback with AI. Tools like tRPC are going to fall out of favor relative to OpenAPI-driven patterns that AI agents can better understand. Separation of UI and business logic is the way forward if you want your apps to be accessible to AI agents.

Solution architecture

The Instantly campaign holds the multi-email sequence and is configured to stop on reply. A lead upload script reads a CSV of contacts, groups them by company, and uploads them to Instantly with a companyName custom variable. Then a webhook server listens for reply events, finds all leads from the same company, and marks them as “not interested” to stop their sequences.
CSV (company, email)
    → Upload Script
    → Instantly (leads with companyName)

Reply received
    → Instantly webhook
    → Webhook server
    → Find leads by companyName
    → Update lead status
    → Sequence stops for whole company

Creating the campaign

The campaign itself is straightforward. Set stop_on_reply to true so Instantly stops the sequence for whoever replies, then define your email steps with delays between them.
curl -X POST https://api.instantly.ai/api/v2/campaigns \
  -H "Authorization: Bearer $INSTANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Feature Launch Outreach",
    "stop_on_reply": true,
    "stop_on_auto_reply": true,
    "sequences": [{
      "steps": [
        {"type": "email", "delay": 0, "variants": [{"subject": "...", "body": "..."}]},
        {"type": "email", "delay": 3, "variants": [{"subject": "...", "body": "..."}]}
      ]
    }]
  }'

Uploading leads

The critical detail is including a companyName custom variable with every lead. The webhook server uses that field to find all leads from the same company and unsubscribe them when a relevant event fires.
import csv
import requests

API_KEY = "your-api-key"
CAMPAIGN_ID = "your-campaign-id"

with open("contacts.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        emails = row["emails"].split(";")
        company = row["company_name"]

        for email in emails:
            requests.post(
                "https://api.instantly.ai/api/v2/leads",
                headers={"Authorization": f"Bearer {API_KEY}"},
                json={
                    "email": email.strip(),
                    "company_name": company,
                    "custom_variables": {"companyName": company},
                    "campaign": CAMPAIGN_ID
                }
            )

Registering the webhook

Tell Instantly to POST to your server whenever someone replies. You’ll need the campaign ID from the previous step.
curl -X POST https://api.instantly.ai/api/v2/webhooks \
  -H "Authorization: Bearer $INSTANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target_hook_url": "https://your-server.com/webhook/reply",
    "event_type": "reply_received",
    "campaign_id": "your-campaign-id"
  }'

Webhook server

This is the part that makes it all work. When anyone replies, the webhook server extracts the company name, queries Instantly for all leads with that company name, and updates each lead’s status to stop their sequence.
const express = require('express');
const app = express();
app.use(express.json());

const API_KEY = process.env.INSTANTLY_API_KEY;
const CAMPAIGN_ID = process.env.CAMPAIGN_ID;

app.post('/webhook/reply', async (req, res) => {
  const event = req.body;

  if (event.event_type !== 'reply_received') {
    return res.json({ status: 'ignored' });
  }

  const leadEmail = event.lead_email;
  const companyName = event.lead?.company_name;

  if (!companyName) {
    return res.json({ status: 'ignored', reason: 'no company' });
  }

  // Find all leads from this company
  const leads = await findLeadsByCompany(companyName);

  // Stop all of them (except the one who replied — they're already stopped)
  for (const lead of leads) {
    if (lead.email !== leadEmail) {
      await stopLead(lead.email);
    }
  }

  res.json({ status: 'ok', stopped: leads.length - 1 });
});

// Instantly's API doesn't let you filter by company_name server-side.
// The search param only works on name/email. So we fetch all leads
// and filter client-side. For large campaigns, you'd want to cache
// this or build your own company->leads index.
async function findLeadsByCompany(companyName) {
  const leads = [];
  let cursor = null;

  do {
    const resp = await fetch('https://api.instantly.ai/api/v2/leads/list', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ campaign: CAMPAIGN_ID, starting_after: cursor })
    });
    const data = await resp.json();
    leads.push(...data.items);
    cursor = data.next_starting_after;
  } while (cursor);

  return leads.filter(lead => lead.company_name === companyName);
}

async function stopLead(email) {
  await fetch('https://api.instantly.ai/api/v2/leads/update-interest-status', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ lead_email: email, interest_value: -1 })
  });
}

app.listen(3000);
Instantly’s API does not support filtering leads by company_name server-side — the search parameter only works on name and email fields. The implementation above fetches all leads for the campaign and filters client-side. For large campaigns you’d want to cache the result or build your own company-to-leads index to avoid repeated full paginations on every reply event.

Hosting options

The server is stateless, so any hosting that can run Node.js works.

Someone please build this

This solution works, but it’s more complex than it should be. The fact that I had to build a webhook server to get org-level behavior is absurd. This should be a checkbox in every B2B email tool. What the ideal interface looks like:
Define groups of contacts by company when setting up a campaign, the same way you’d define a segment. The tool owns the grouping — you don’t have to encode it in a custom variable and maintain a separate lookup.
A single boolean on the campaign: if any member of a company group takes action (replies, clicks, converts), the tool stops the sequence for all remaining members of that group automatically.
The entire org-level stopping logic should live inside the email tool itself. A webhook server, client-side lead pagination, and a custom interest-status update should not be necessary for this use case.
If you’re building B2B email tooling, please add this. It’s a genuinely common pattern for any feature launch or outreach campaign that targets multiple stakeholders per account.