Claude-code-plugins-plus-skills clay-sdk-patterns
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/clay-pack/skills/clay-sdk-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-clay-sdk-patterns && rm -rf "$T"
manifest:
plugins/saas-packs/clay-pack/skills/clay-sdk-patterns/SKILL.mdsource content
Clay Integration Patterns
Overview
Production-ready patterns for Clay integrations. Clay does not have an official SDK -- you interact via webhooks (inbound), HTTP API enrichment columns (outbound from Clay), and the Enterprise API (programmatic lookups). These patterns wrap those interfaces into reliable, reusable code.
Prerequisites
- Completed
setupclay-install-auth - Familiarity with async/await patterns
- Understanding of Clay's webhook and HTTP API model
Instructions
Step 1: Create a Clay Webhook Client (TypeScript)
// src/clay/client.ts — typed wrapper for Clay webhook and Enterprise API interface ClayConfig { webhookUrl: string; // Table's webhook URL for inbound data enterpriseApiKey?: string; // Enterprise API key (optional) baseUrl?: string; // Default: https://api.clay.com maxRetries?: number; timeoutMs?: number; } class ClayClient { private config: Required<ClayConfig>; constructor(config: ClayConfig) { this.config = { baseUrl: 'https://api.clay.com', maxRetries: 3, timeoutMs: 30_000, enterpriseApiKey: '', ...config, }; } /** Send a record to a Clay table via webhook */ async sendToTable(data: Record<string, unknown>): Promise<void> { const res = await this.fetchWithRetry(this.config.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) { throw new ClayWebhookError(`Webhook failed: ${res.status}`, res.status); } } /** Send multiple records in sequence with rate limiting */ async sendBatch(rows: Record<string, unknown>[], delayMs = 200): Promise<BatchResult> { const results: BatchResult = { sent: 0, failed: 0, errors: [] }; for (const row of rows) { try { await this.sendToTable(row); results.sent++; } catch (err) { results.failed++; results.errors.push({ row, error: (err as Error).message }); } if (delayMs > 0) await new Promise(r => setTimeout(r, delayMs)); } return results; } /** Enterprise API: Enrich a person by email (Enterprise plan only) */ async enrichPerson(email: string): Promise<PersonEnrichment> { if (!this.config.enterpriseApiKey) { throw new Error('Enterprise API key required for person enrichment'); } const res = await this.fetchWithRetry(`${this.config.baseUrl}/v1/people/enrich`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.enterpriseApiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ email }), }); return res.json(); } /** Enterprise API: Enrich a company by domain (Enterprise plan only) */ async enrichCompany(domain: string): Promise<CompanyEnrichment> { if (!this.config.enterpriseApiKey) { throw new Error('Enterprise API key required for company enrichment'); } const res = await this.fetchWithRetry(`${this.config.baseUrl}/v1/companies/enrich`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.enterpriseApiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ domain }), }); return res.json(); } private async fetchWithRetry(url: string, init: RequestInit): Promise<Response> { for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs); const res = await fetch(url, { ...init, signal: controller.signal }); clearTimeout(timeout); if (res.status === 429) { const retryAfter = parseInt(res.headers.get('Retry-After') || '5'); await new Promise(r => setTimeout(r, retryAfter * 1000)); continue; } return res; } catch (err) { if (attempt === this.config.maxRetries) throw err; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); } } throw new Error('Max retries exceeded'); } }
Step 2: Type Definitions for Clay Data
// src/clay/types.ts interface PersonEnrichment { name?: string; email?: string; title?: string; company?: string; linkedin_url?: string; location?: string; } interface CompanyEnrichment { name?: string; domain?: string; industry?: string; employee_count?: number; linkedin_url?: string; location?: string; description?: string; } interface BatchResult { sent: number; failed: number; errors: Array<{ row: Record<string, unknown>; error: string }>; } class ClayWebhookError extends Error { constructor(message: string, public statusCode: number) { super(message); this.name = 'ClayWebhookError'; } }
Step 3: Python Client
# clay_client.py — Python wrapper for Clay webhook and Enterprise API import httpx import asyncio from dataclasses import dataclass, field from typing import Any @dataclass class ClayClient: webhook_url: str enterprise_api_key: str = "" base_url: str = "https://api.clay.com" max_retries: int = 3 timeout: float = 30.0 async def send_to_table(self, data: dict[str, Any]) -> None: """Send a single record to a Clay table via webhook.""" async with httpx.AsyncClient(timeout=self.timeout) as client: for attempt in range(self.max_retries + 1): response = await client.post( self.webhook_url, json=data, headers={"Content-Type": "application/json"}, ) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", "5")) await asyncio.sleep(retry_after) continue response.raise_for_status() return raise Exception("Max retries exceeded") async def send_batch(self, rows: list[dict], delay: float = 0.2) -> dict: """Send multiple records with rate limiting.""" results = {"sent": 0, "failed": 0, "errors": []} for row in rows: try: await self.send_to_table(row) results["sent"] += 1 except Exception as e: results["failed"] += 1 results["errors"].append({"row": row, "error": str(e)}) await asyncio.sleep(delay) return results async def enrich_person(self, email: str) -> dict: """Enterprise API: Look up person data by email.""" async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.post( f"{self.base_url}/v1/people/enrich", json={"email": email}, headers={"Authorization": f"Bearer {self.enterprise_api_key}"}, ) response.raise_for_status() return response.json()
Step 4: Singleton Pattern for Multi-Use
// src/clay/instance.ts — reuse a single client across your app let instance: ClayClient | null = null; export function getClayClient(): ClayClient { if (!instance) { instance = new ClayClient({ webhookUrl: process.env.CLAY_WEBHOOK_URL!, enterpriseApiKey: process.env.CLAY_API_KEY, }); } return instance; }
Error Handling
| Pattern | Use Case | Benefit |
|---|---|---|
| Retry with backoff | 429 rate limits, network errors | Automatic recovery |
| Batch with delay | Sending many rows | Respects Clay rate limits |
| Enterprise API guard | Missing API key | Clear error before API call |
| Timeout control | Slow webhook delivery | Prevents hung connections |
Examples
Webhook Handler for Clay Callbacks
// Express handler for Clay HTTP API column callbacks app.post('/api/clay/callback', (req, res) => { // Respond 200 immediately (Clay expects fast response) res.json({ ok: true }); // Process async processEnrichedData(req.body).catch(console.error); });
Resources
- Clay University -- HTTP API Integration
- Clay University -- Webhook Guide
- Clay University -- Using Clay as an API
Next Steps
Apply patterns in
clay-core-workflow-a for real-world lead enrichment.