Ctrl + k

or run

tessl search
Log in

apollo-webhooks-events

tessl install github:jeremylongshore/claude-code-plugins-plus-skills --skill apollo-webhooks-events
github.com/jeremylongshore/claude-code-plugins-plus-skills

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".

Review Score

84%

Validation Score

13/16

Implementation Score

77%

Activation Score

90%

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 TypeDescriptionPayload Contains
contact.createdNew contact addedContact data
contact.updatedContact info changedUpdated fields
sequence.startedContact added to sequenceSequence & contact IDs
sequence.completedSequence finishedCompletion status
email.sentEmail deliveredEmail & contact info
email.openedEmail was openedOpen timestamp
email.clickedLink clickedClick details
email.repliedReply receivedReply content
email.bouncedEmail bouncedBounce 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

IssueResolution
Invalid signatureCheck webhook secret
Unknown eventLog and acknowledge (200)
Processing errorLog error, return 500
Duplicate eventsImplement idempotency

Resources

  • Apollo Webhooks Documentation
  • Webhook Security Best Practices
  • ngrok for Local Testing

Next Steps

Proceed to apollo-performance-tuning for optimization.