Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Clay webhook receivers and HTTP API column callbacks for real-time data flow. Use when setting up webhook endpoints, handling enrichment callbacks from Clay, or building event-driven integrations with Clay tables. Trigger with phrases like "clay webhook", "clay events", "clay callback", "handle clay data", "clay notifications", "clay HTTP API column".
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 22% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 239% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 104% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 80% | 0% |
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.
Every Clay table has a unique webhook URL. When you POST JSON to this URL, a new row appears in the table.
typescript// 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: 'cto@acme.com', domain: 'acme.com', first_name: 'Jane', last_name: 'Doe', source: 'website-form', });
Clay's HTTP API enrichment column POSTs data to your endpoint after enrichment runs. Set up a handler:
typescript// 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})`); } }
In your Clay table, add an HTTP API enrichment column:
https://your-app.com/api/clay/enrichedContent-Type: application/jsonX-Clay-Signature: {{shared-secret-hash}} (optional)json{ "email": "{{Work Email}}", "first_name": "{{first_name}}", "last_name": "{{last_name}}", "company_name": "{{Company Name}}", "job_title": "{{Job Title}}", "employee_count": "{{Employee Count}}", "industry": "{{Industry}}", "linkedin_url": "{{LinkedIn URL}}", "icp_score": "{{ICP Score}}", "personalized_opener": "{{Personalized Opener}}" }
ISNOTEMPTY(Work Email) AND ICP Score >= 50Clay may retry failed HTTP API calls. Ensure idempotent handling:
typescript// src/clay/idempotency.ts const processedSet = new Set<string>(); function getIdempotencyKey(lead: EnrichedLead): string { return crypto.createHash('sha256') .update(`${lead.email}:${lead.company_name}:${Date.now().toString().slice(0, -4)}`) .digest('hex'); } async function processIdempotent(lead: EnrichedLead): Promise<boolean> { const key = getIdempotencyKey(lead); if (processedSet.has(key)) { console.log(`Duplicate callback skipped: ${lead.email}`); return false; } processedSet.add(key); await processEnrichedLead(lead); return true; }
typescript// src/clay/integrations.ts — push enriched leads to downstream tools // Zapier webhook trigger async function triggerZapier(lead: EnrichedLead): Promise<void> { await fetch(process.env.ZAPIER_WEBHOOK_URL!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(lead), }); } // Slack notification for high-value leads async function notifySlack(lead: EnrichedLead): Promise<void> { if (!lead.icp_score || lead.icp_score < 90) return; await fetch(process.env.SLACK_WEBHOOK_URL!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `Hot lead from Clay: ${lead.first_name} ${lead.last_name} (${lead.job_title}) at ${lead.company_name} - ICP Score: ${lead.icp_score}`, }), }); }
| Issue | Cause | Solution | |-------|-------|----------| | Webhook returns 404 | URL expired or table deleted | Re-create webhook in Clay table | | HTTP API column shows error | Your endpoint unreachable | Verify HTTPS URL is publicly accessible | | Duplicate callbacks | Clay retried failed request | Implement idempotency (Step 4) | | Webhook 50K limit hit | High volume usage | Create new webhook on same table | | Callback timeout | Slow processing | Respond 200 immediately, process async |
For performance optimization, see clay-performance-tuning.
Other measured skills in the registry, with their headline benchmark lift.