home / skills / jeremylongshore / claude-code-plugins-plus-skills / apollo-webhooks-events

This skill helps you handle Apollo.io webhooks to process contact, sequence, and email events and synchronize data in real time.

npx playbooks add skill jeremylongshore/claude-code-plugins-plus-skills --skill apollo-webhooks-events

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
11.7 KB
---
name: apollo-webhooks-events
description: |
  Implement Apollo.io webhook handling.
  Use when receiving Apollo webhooks, processing event notifications,
  or building event-driven integrations.
  Trigger with phrases like "apollo webhooks", "apollo events",
  "apollo notifications", "apollo webhook handler", "apollo triggers".
allowed-tools: Read, Write, Edit, Bash(gh:*), Bash(curl:*)
version: 1.0.0
license: MIT
author: Jeremy Longshore <[email protected]>
---

# 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 |
|------------|-------------|------------------|
| `contact.created` | New contact added | Contact data |
| `contact.updated` | Contact info changed | Updated fields |
| `sequence.started` | Contact added to sequence | Sequence & contact IDs |
| `sequence.completed` | Sequence finished | Completion status |
| `email.sent` | Email delivered | Email & contact info |
| `email.opened` | Email was opened | Open timestamp |
| `email.clicked` | Link clicked | Click details |
| `email.replied` | Reply received | Reply content |
| `email.bounced` | Email bounced | Bounce reason |

## Webhook Handler Implementation

### Express Handler
```typescript
// 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
```typescript
// 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

```typescript
// 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

```typescript
// 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: '[email protected]',
          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

```bash
# 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
- [Apollo Webhooks Documentation](https://knowledge.apollo.io/hc/en-us/articles/4415154183053)
- [Webhook Security Best Practices](https://hookdeck.com/webhooks/guides/webhook-security-best-practices)
- [ngrok for Local Testing](https://ngrok.com/)

## Next Steps
Proceed to `apollo-performance-tuning` for optimization.

Overview

This skill implements a robust Apollo.io webhook handler to receive real-time notifications for contact updates, sequence events, and email engagement. It provides signature verification, event validation, and pluggable handlers that sync data to your database and publish internal events for downstream processing. The skill includes local testing tips and guidance for registering webhooks in the Apollo UI.

How this skill works

The handler verifies incoming requests using an HMAC-SHA256 signature against APOLLO_WEBHOOK_SECRET to ensure authenticity. Incoming payloads are validated by event type and routed to specific handlers for contact, sequence, and email events. Handlers upsert contacts, record sequence enrollments, log email engagements, and emit internal events for notifications or downstream workflows.

When to use it

  • Receiving Apollo webhooks in a production or staging server
  • Syncing Apollo contact and engagement data into your local database
  • Triggering internal workflows when sequences start or complete
  • Notifying sales or support teams on email replies or bounces
  • Testing webhook handling locally via ngrok and automated tests

Best practices

  • Store APOLLO_WEBHOOK_SECRET in environment variables and never commit it
  • Use timing-safe comparison for signature verification to prevent timing attacks
  • Validate payload shape per event type and fail early on invalid data
  • Implement idempotency when writing to the database to handle duplicate webhooks
  • Log unknown event types and return 200 to acknowledge receipt while avoiding retries

Example use cases

  • Sync a newly created Apollo contact into the CRM with upsert logic
  • Record email opens and clicks to an analytics table for campaign reporting
  • Mark contacts as bounced and trigger cleanup workflows for invalid emails
  • Create sequence enrollment records when a contact enters a campaign
  • Emit 'lead engaged' notifications to Slack or a sales queue when replies arrive

FAQ

How do I register webhooks with Apollo?

Register webhooks through Apollo Settings > Integrations > Webhooks. Add your endpoint URL, select events, copy the provided secret, and set APOLLO_WEBHOOK_SECRET in your environment.

What should I do if I receive duplicate events?

Implement idempotency using unique event IDs or by checking last-updated timestamps before applying changes to your database.