Implement Clay webhook signature validation and event handling.
Use when setting up webhook endpoints, implementing signature verification,
or handling Clay event notifications securely.
Trigger with phrases like "clay webhook", "clay events",
"clay webhook signature", "handle clay events", "clay notifications".
Clay's event-driven architecture has two webhook patterns: (1) Inbound webhooks -- you POST data into Clay tables via unique webhook URLs, and (2) Outbound HTTP API columns -- Clay POSTs enriched data to your endpoint after enrichment completes. This skill covers both patterns with production-ready handlers.
Prerequisites
Clay table with webhook source configured (for inbound)
Clay table with HTTP API enrichment column (for outbound)
HTTPS endpoint accessible from the internet
Familiarity with Express.js or similar framework
Instructions
Step 1: Inbound Webhook -- Send Data into Clay
Every Clay table has a unique webhook URL. When you POST JSON to this URL, a new row appears in the table.
// src/clay/inbound.ts — send data into Clay tables
class ClayInboundWebhook {
constructor(
private webhookUrl: string,
private submissionCount: number = 0,
private readonly LIMIT: number = 50_000,
) {}
async sendRow(data: Record<string, unknown>): Promise<void> {
if (this.submissionCount >= this.LIMIT) {
throw new Error(`Webhook exhausted (${this.LIMIT} submissions). Create a new webhook in Clay.`);
}
const res = await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`Clay webhook failed: ${res.status} ${res.statusText}`);
}
this.submissionCount++;
}
async sendBatch(rows: Record<string, unknown>[], delayMs = 200): Promise<{ sent: number; failed: number }> {
let sent = 0, failed = 0;
for (const row of rows) {
try {
await this.sendRow(row);
sent++;
} catch {
failed++;
}
await new Promise(r => setTimeout(r, delayMs));
}
return { sent, failed };
}
}
// Usage
const webhook = new ClayInboundWebhook(process.env.CLAY_WEBHOOK_URL!);
await webhook.sendRow({
email: '[email protected]',
domain: 'acme.com',
first_name: 'Jane',
last_name: 'Doe',
source: 'website-form',
});
Step 2: Outbound Callback -- Receive Enriched Data from Clay
Clay's HTTP API enrichment column POSTs data to your endpoint after enrichment runs. Set up a handler:
// src/clay/outbound-handler.ts — receive enriched data from Clay
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json({ limit: '1mb' }));
// Signature verification middleware
function verifyClaySignature(req: any, res: any, next: any) {
const signature = req.headers['x-clay-signature'];
const secret = process.env.CLAY_WEBHOOK_SECRET;
if (secret && signature) {
const expected = crypto.createHmac('sha256', secret)
.update(JSON.stringify(req.body))
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
next();
}
// Main callback endpoint
app.post('/api/clay/enriched', verifyClaySignature, async (req, res) => {
// Respond 200 immediately — Clay expects fast response
res.json({ received: true, timestamp: new Date().toISOString() });
// Process enriched data async
try {
await processEnrichedLead(req.body);
} catch (err) {
console.error('Failed to process enriched lead:', err);
}
});
interface EnrichedLead {
email?: string;
first_name?: string;
last_name?: string;
company_name?: string;
job_title?: string;
employee_count?: number;
industry?: string;
linkedin_url?: string;
icp_score?: number;
personalized_opener?: string;
}
async function processEnrichedLead(lead: EnrichedLead): Promise<void> {
// Route based on ICP score
if (lead.icp_score && lead.icp_score >= 80 && lead.email) {
await pushToOutreachSequence(lead);
} else if (lead.icp_score && lead.icp_score >= 50) {
await addToNurtureCampaign(lead);
} else {
console.log(`Low-score lead skipped: ${lead.email} (score: ${lead.icp_score})`);
}
}
Step 3: Configure the HTTP API Column in Clay
In your Clay table, add an HTTP API enrichment column: