Woodpecker.co is a cold email platform built for B2B SaaS sales — sequences, follow-ups, A/B testing, and team inbox rotation. GitLeads plugs directly into Woodpecker via its REST API: every developer who shows a GitHub buying signal (new star, keyword mention) becomes a Woodpecker prospect, enriched and ready for your sequence.
What You Get in Each Lead
- GitHub username, name, email (if public), company, location
- Signal type: stargazer (starred your or a competitor repo) or keyword (mentioned your target term in an issue/PR/discussion)
- Signal context: the exact repo or keyword that triggered the lead
- Top programming languages — useful for sequence personalization
- Follower count — a proxy for developer influence
Woodpecker API Integration
Woodpecker exposes a REST API for adding prospects to campaigns. The flow: GitLeads fires a webhook → your handler validates the payload → calls Woodpecker API to add the prospect → Woodpecker starts the email sequence automatically.
import crypto from 'crypto';
interface GitLeadsLead {
name: string;
email?: string;
githubUsername: string;
profileUrl: string;
bio?: string;
company?: string;
location?: string;
followers: number;
topLanguages: string[];
signalType: 'stargazer' | 'keyword';
signalContext: string;
}
function verifyGitleadsSignature(body: string, sig: string, secret: string) {
const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
async function addProspectToWoodpecker(lead: GitLeadsLead): Promise<void> {
const WOODPECKER_API_KEY = process.env.WOODPECKER_API_KEY!;
const CAMPAIGN_ID = parseInt(process.env.WOODPECKER_CAMPAIGN_ID!, 10);
const res = await fetch('https://api.woodpecker.co/rest/v1/prospects_lists', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${WOODPECKER_API_KEY}`,
},
body: JSON.stringify({
update: false, // don't overwrite existing prospects
campaign: { id: CAMPAIGN_ID },
prospects: [
{
email: lead.email!,
first_name: lead.name.split(' ')[0] ?? lead.githubUsername,
last_name: lead.name.split(' ').slice(1).join(' ') || '',
company: lead.company ?? '',
// Woodpecker custom variables — map to {{VAR}} in templates
snippet1: lead.githubUsername, // {{SNIPPET1}} = GitHub username
snippet2: lead.topLanguages.join(', '), // {{SNIPPET2}} = top languages
snippet3: lead.signalContext, // {{SNIPPET3}} = signal context
snippet4: lead.signalType, // {{SNIPPET4}} = signal type
snippet5: String(lead.followers), // {{SNIPPET5}} = follower count
},
],
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Woodpecker API error: ${err}`);
}
}
export async function POST(req: Request): Promise<Response> {
const rawBody = await req.text();
const sig = req.headers.get('x-gitleads-signature') ?? '';
if (!verifyGitleadsSignature(rawBody, sig, process.env.GITLEADS_WEBHOOK_SECRET!)) {
return new Response('Unauthorized', { status: 401 });
}
const { event, lead } = JSON.parse(rawBody) as {
event: string;
lead: GitLeadsLead;
};
if (event === 'lead.created' && lead.email) {
await addProspectToWoodpecker(lead);
}
return new Response('ok');
}Personalizing Woodpecker Templates with GitHub Data
Woodpecker supports custom snippet variables in email templates. Map GitLeads fields to snippets and reference them in subject lines and body copy:
- Subject: "Quick question about your work on {{SNIPPET3}}" — references the repo or keyword
- Opener: "Noticed you're working in {{SNIPPET2}} — we built something that might save you a few hours..."
- Social proof: "{{SNIPPET5}} developers on GitHub are already using [your product]..."
- CTA variation: star signals → evaluation angle; keyword signals → pain-point angle
Routing Leads to the Right Campaign
Use different CAMPAIGN_IDs based on signal type. Stargazers are evaluating — use a shorter, more direct sequence. Keyword mentions signal a specific pain point — use a solution-focused sequence.
const CAMPAIGN_MAP: Record<string, number> = {
stargazer: parseInt(process.env.WOODPECKER_STARGAZER_CAMPAIGN_ID!, 10),
keyword: parseInt(process.env.WOODPECKER_KEYWORD_CAMPAIGN_ID!, 10),
};
// In your webhook handler:
const campaignId = CAMPAIGN_MAP[lead.signalType] ?? CAMPAIGN_MAP.stargazer;