Why Loops for Developer Leads
Loops.so is an email platform built specifically for SaaS companies and developer tools. Unlike general-purpose marketing tools, Loops treats transactional and marketing email as one unified system, with a developer-first REST API and a contact model centered on user activity events rather than static lists. When a developer stars your competitor's repo or mentions your category keyword in a GitHub discussion, Loops lets you trigger a targeted email sequence immediately — no manual import, no batch delays.
How the Data Flows
GitLeads fires a JSON webhook for every captured lead. That payload carries the developer's GitHub profile data plus the signal context. Your handler reads it, creates or updates a Loops contact, then sends a Loops event to trigger a sequence. The contact record stores GitHub-enriched attributes — username, company, top languages, signal type — that you can reference in email personalization.
// Node.js handler: GitLeads webhook → Loops contact + event
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const LOOPS_API_KEY = process.env.LOOPS_API_KEY!;
const GITLEADS_SECRET = process.env.GITLEADS_WEBHOOK_SECRET!;
function verify(sig: string, body: string): boolean {
const expected = crypto.createHmac('sha256', GITLEADS_SECRET).update(body).digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post('/webhook/gitleads', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-gitleads-signature'] as string;
if (!verify(sig, req.body.toString())) return res.status(401).send('invalid signature');
const lead = JSON.parse(req.body.toString()) as {
github_username: string;
name: string;
email: string;
company: string;
top_languages: string[];
followers: number;
signal: { type: string; keyword?: string; repo: string; context: string };
};
// Skip leads with no email — Loops requires an email address
if (!lead.email) return res.send('no email, skipped');
// Step 1: create or update the contact in Loops
await fetch('https://app.loops.so/api/v1/contacts/update', {
method: 'PUT',
headers: { Authorization: `Bearer ${LOOPS_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: lead.email,
firstName: (lead.name || lead.github_username).split(' ')[0],
lastName: (lead.name || '').split(' ').slice(1).join(' ') || undefined,
company: lead.company || undefined,
// Custom contact properties for personalization in email copy
githubUsername: lead.github_username,
githubFollowers: lead.followers,
topLanguage: lead.top_languages?.[0] || undefined,
signalType: lead.signal.type,
signalRepo: lead.signal.repo,
signalKeyword: lead.signal.keyword || undefined,
source: 'gitleads',
}),
});
// Step 2: send a Loops event to trigger the right sequence
// Create separate Loops events per signal type for targeted sequences
const eventName = lead.signal.type === 'stargazer'
? 'github_stargazer_captured'
: 'github_keyword_captured';
await fetch('https://app.loops.so/api/v1/events/send', {
method: 'POST',
headers: { Authorization: `Bearer ${LOOPS_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: lead.email,
eventName,
// Event properties available as variables inside Loops email templates
eventProperties: {
signal_context: lead.signal.context.slice(0, 500),
signal_repo: lead.signal.repo,
signal_keyword: lead.signal.keyword,
},
}),
});
res.send('ok');
});
app.listen(3000);Setting Up in GitLeads
- In your GitLeads dashboard go to Settings → Integrations → Webhook
- Enter your endpoint URL and copy the signing secret into GITLEADS_WEBHOOK_SECRET
- Generate a Loops API key at app.loops.so → Settings → API — store as LOOPS_API_KEY
- Create two Loops events in the Loops dashboard: github_stargazer_captured and github_keyword_captured
- Attach email sequences to each event — stargazers get a sequence explaining your product, keyword signals get a more targeted message referencing the problem they mentioned
Personalizing the Emails
Loops email templates use Handlebars-style variables. Because you stored GitHub data as contact properties, your subject line can reference them: "Hey {{firstName}}, saw you're working with {{topLanguage}}" or "Noticed you starred {{signalRepo}} — here's how [product] fits." The signal_context event property lets you reference the exact GitHub issue or PR text that triggered the capture, making outreach feel like a direct response rather than a cold email.
Handling Leads Without Public Emails
Many GitHub profiles do not have a public email. GitLeads includes email when available. For leads without email, the above handler skips the Loops contact creation. Common patterns for these leads: route them to Clay for enrichment (which can find work emails from company + name), or push them to Slack for manual review by DevRel. You can also use Loops with verified email from enrichment downstream.