Claude-skill-registry apollo-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/apollo-webhooks-events" ~/.claude/skills/majiayu000-claude-skill-registry-apollo-webhooks-events && rm -rf "$T"
manifest:
skills/data/apollo-webhooks-events/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
Apollo Webhooks Events
Overview
Implement webhook handlers for Apollo.io to receive real-time notifications about contact updates, sequence events, and engagement activities.
Apollo Webhook Events
| Event Type | Description | Payload Contains |
|---|---|---|
| New contact added | Contact data |
| Contact info changed | Updated fields |
| Contact added to sequence | Sequence & contact IDs |
| Sequence finished | Completion status |
| Email delivered | Email & contact info |
| Email was opened | Open timestamp |
| Link clicked | Click details |
| Reply received | Reply content |
| Email bounced | Bounce reason |
Webhook Handler Implementation
Express Handler
// src/routes/webhooks/apollo.ts import { Router } from 'express'; import crypto from 'crypto'; import { z } from 'zod'; const router = Router(); // Webhook payload schemas const ContactEventSchema = z.object({ event: z.enum(['contact.created', 'contact.updated']), timestamp: z.string(), data: z.object({ contact: z.object({ id: z.string(), email: z.string().optional(), name: z.string().optional(), title: z.string().optional(), organization: z.object({ name: z.string(), }).optional(), }), changes: z.record(z.any()).optional(), }), }); const SequenceEventSchema = z.object({ event: z.enum(['sequence.started', 'sequence.completed', 'sequence.paused']), timestamp: z.string(), data: z.object({ sequence_id: z.string(), contact_id: z.string(), status: z.string().optional(), }), }); const EmailEventSchema = z.object({ event: z.enum(['email.sent', 'email.opened', 'email.clicked', 'email.replied', 'email.bounced']), timestamp: z.string(), data: z.object({ email_id: z.string(), contact_id: z.string(), sequence_id: z.string().optional(), subject: z.string().optional(), link_url: z.string().optional(), // For click events bounce_reason: z.string().optional(), // For bounce events }), }); // Verify webhook signature function verifySignature(payload: string, signature: string, secret: string): boolean { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } // Middleware for signature verification function verifyApolloWebhook(req: any, res: any, next: any) { const signature = req.headers['x-apollo-signature']; const webhookSecret = process.env.APOLLO_WEBHOOK_SECRET; if (!webhookSecret) { console.error('APOLLO_WEBHOOK_SECRET not configured'); return res.status(500).json({ error: 'Webhook secret not configured' }); } if (!signature) { return res.status(401).json({ error: 'Missing signature' }); } const rawBody = JSON.stringify(req.body); if (!verifySignature(rawBody, signature, webhookSecret)) { return res.status(401).json({ error: 'Invalid signature' }); } next(); } // Main webhook endpoint router.post('/apollo', verifyApolloWebhook, async (req, res) => { const { event } = req.body; try { // Route to appropriate handler if (event.startsWith('contact.')) { await handleContactEvent(ContactEventSchema.parse(req.body)); } else if (event.startsWith('sequence.')) { await handleSequenceEvent(SequenceEventSchema.parse(req.body)); } else if (event.startsWith('email.')) { await handleEmailEvent(EmailEventSchema.parse(req.body)); } else { console.warn('Unknown event type:', event); } res.status(200).json({ received: true }); } catch (error: any) { console.error('Webhook processing error:', error); res.status(400).json({ error: error.message }); } }); export default router;
Event Handlers
// src/services/webhooks/handlers.ts import { prisma } from '../db'; import { publishEvent } from '../events'; export async function handleContactEvent(payload: any) { const { event, data } = payload; switch (event) { case 'contact.created': // Sync new contact to local database await prisma.contact.upsert({ where: { apolloId: data.contact.id }, create: { apolloId: data.contact.id, email: data.contact.email, name: data.contact.name, title: data.contact.title, company: data.contact.organization?.name, syncedAt: new Date(), }, update: { email: data.contact.email, name: data.contact.name, title: data.contact.title, company: data.contact.organization?.name, syncedAt: new Date(), }, }); await publishEvent('apollo.contact.synced', { contactId: data.contact.id, action: 'created', }); break; case 'contact.updated': await prisma.contact.update({ where: { apolloId: data.contact.id }, data: { ...data.changes, syncedAt: new Date(), }, }); await publishEvent('apollo.contact.synced', { contactId: data.contact.id, action: 'updated', changes: data.changes, }); break; } } export async function handleSequenceEvent(payload: any) { const { event, data } = payload; switch (event) { case 'sequence.started': await prisma.sequenceEnrollment.create({ data: { apolloContactId: data.contact_id, apolloSequenceId: data.sequence_id, status: 'active', startedAt: new Date(), }, }); break; case 'sequence.completed': await prisma.sequenceEnrollment.update({ where: { apolloContactId_apolloSequenceId: { apolloContactId: data.contact_id, apolloSequenceId: data.sequence_id, }, }, data: { status: data.status || 'completed', completedAt: new Date(), }, }); break; } } export async function handleEmailEvent(payload: any) { const { event, data, timestamp } = payload; // Record email engagement await prisma.emailEngagement.create({ data: { apolloEmailId: data.email_id, apolloContactId: data.contact_id, apolloSequenceId: data.sequence_id, eventType: event.replace('email.', ''), eventData: { subject: data.subject, linkUrl: data.link_url, bounceReason: data.bounce_reason, }, occurredAt: new Date(timestamp), }, }); // Handle specific events if (event === 'email.replied') { // Notify sales team await publishEvent('apollo.lead.engaged', { contactId: data.contact_id, type: 'reply', }); } else if (event === 'email.bounced') { // Mark contact as bounced await prisma.contact.update({ where: { apolloId: data.contact_id }, data: { emailStatus: 'bounced' }, }); } }
Webhook Registration
// scripts/register-webhooks.ts import { apollo } from '../src/lib/apollo/client'; interface WebhookConfig { url: string; events: string[]; secret: string; } async function registerWebhook(config: WebhookConfig) { // Note: Apollo webhook registration is typically done through the UI // This is a placeholder for future API support console.log('Webhook registration:', config); // For now, provide instructions console.log(` To register webhooks in Apollo: 1. Go to Apollo Settings > Integrations > Webhooks 2. Click "Add Webhook" 3. Enter URL: ${config.url} 4. Select events: ${config.events.join(', ')} 5. Copy the webhook secret and add to your environment: APOLLO_WEBHOOK_SECRET=<secret> `); } const webhookConfig: WebhookConfig = { url: `${process.env.APP_URL}/webhooks/apollo`, events: [ 'contact.created', 'contact.updated', 'sequence.started', 'sequence.completed', 'email.sent', 'email.opened', 'email.clicked', 'email.replied', 'email.bounced', ], secret: process.env.APOLLO_WEBHOOK_SECRET!, }; registerWebhook(webhookConfig);
Testing Webhooks
// tests/webhooks/apollo.test.ts import { describe, it, expect } from 'vitest'; import request from 'supertest'; import crypto from 'crypto'; import app from '../../src/app'; function signPayload(payload: any, secret: string): string { return crypto .createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('hex'); } describe('Apollo Webhooks', () => { const secret = 'test-webhook-secret'; beforeAll(() => { process.env.APOLLO_WEBHOOK_SECRET = secret; }); it('rejects requests without signature', async () => { const response = await request(app) .post('/webhooks/apollo') .send({ event: 'contact.created' }); expect(response.status).toBe(401); }); it('rejects requests with invalid signature', async () => { const response = await request(app) .post('/webhooks/apollo') .set('x-apollo-signature', 'invalid') .send({ event: 'contact.created' }); expect(response.status).toBe(401); }); it('processes contact.created event', async () => { const payload = { event: 'contact.created', timestamp: new Date().toISOString(), data: { contact: { id: 'test-123', email: 'test@example.com', name: 'Test User', }, }, }; const signature = signPayload(payload, secret); const response = await request(app) .post('/webhooks/apollo') .set('x-apollo-signature', signature) .send(payload); expect(response.status).toBe(200); expect(response.body.received).toBe(true); }); it('processes email.opened event', async () => { const payload = { event: 'email.opened', timestamp: new Date().toISOString(), data: { email_id: 'email-123', contact_id: 'contact-123', sequence_id: 'seq-123', }, }; const signature = signPayload(payload, secret); const response = await request(app) .post('/webhooks/apollo') .set('x-apollo-signature', signature) .send(payload); expect(response.status).toBe(200); }); });
Local Testing with ngrok
# Start local server npm run dev # In another terminal, start ngrok ngrok http 3000 # Use the ngrok URL for webhook registration # Example: https://abc123.ngrok.io/webhooks/apollo
Output
- Webhook endpoint with signature verification
- Event handlers for all Apollo event types
- Database sync for contact and engagement data
- Webhook registration instructions
- Test suite for webhook validation
Error Handling
| Issue | Resolution |
|---|---|
| Invalid signature | Check webhook secret |
| Unknown event | Log and acknowledge (200) |
| Processing error | Log error, return 500 |
| Duplicate events | Implement idempotency |
Resources
Next Steps
Proceed to
apollo-performance-tuning for optimization.