Skillshub attio-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/attio-webhooks-events" ~/.claude/skills/comeonoliver-skillshub-attio-webhooks-events && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/attio-webhooks-events/SKILL.mdsource content
Attio Webhooks & Events
Overview
Attio v2 webhooks deliver real-time event notifications to your HTTPS endpoint. You can subscribe to specific event types and filter by object, list, or attribute to reduce volume. Webhooks are managed via
POST /v2/webhooks and verified with HMAC-SHA256 signatures.
Prerequisites
- HTTPS endpoint accessible from the internet
- Scopes:
webhook:read-write
stored securelyATTIO_WEBHOOK_SECRET
Attio Webhook Event Types
| Category | Event types |
|---|---|
| Record | , , , |
| List Entry | , , |
| Note | , , |
| Task | , , |
| Comment | , , |
| List | , , |
| Object Attribute | , |
| List Attribute | , |
| Workspace Member | , |
| Call Recording | , |
Instructions
Step 1: Create a Webhook Subscription
// Register webhook with event filtering const webhook = await client.post<{ data: { id: { workspace_id: string; webhook_id: string }; target_url: string; subscriptions: Array<{ event_type: string; filter?: object }>; created_at: string; }; }>("/webhooks", { target_url: "https://yourapp.com/api/webhooks/attio", subscriptions: [ // All record events { event_type: "record.created" }, { event_type: "record.updated" }, { event_type: "record.deleted" }, // Only deal list-entry events (filtered) { event_type: "list-entry.created", filter: { list: { $eq: "sales_pipeline" } }, }, // Only updates to a specific attribute { event_type: "record.updated", filter: { $and: [ { object: { $eq: "deals" } }, { attribute: { $eq: "stage" } }, ], }, }, // Notes and tasks { event_type: "note.created" }, { event_type: "task.created" }, ], }); console.log("Webhook ID:", webhook.data.id.webhook_id);
Step 2: List and Manage Webhooks
// List all webhooks const webhooks = await client.get<{ data: Array<{ id: { webhook_id: string }; target_url: string; subscriptions: Array<{ event_type: string }>; }>; }>("/webhooks"); // Get a specific webhook const wh = await client.get(`/webhooks/${webhookId}`); // Update webhook subscriptions await client.patch(`/webhooks/${webhookId}`, { subscriptions: [ { event_type: "record.created" }, { event_type: "record.updated" }, ], }); // Delete a webhook await client.delete(`/webhooks/${webhookId}`);
Step 3: Webhook Endpoint with Signature Verification
import express from "express"; import crypto from "crypto"; const app = express(); // CRITICAL: Use raw body for signature verification app.post( "/api/webhooks/attio", express.raw({ type: "application/json" }), async (req, res) => { const signature = req.headers["x-attio-signature"] as string; const timestamp = req.headers["x-attio-timestamp"] as string; // 1. Verify signature if (!verifyAttioWebhook(req.body, signature, timestamp)) { console.error("Webhook signature verification failed"); return res.status(401).json({ error: "Invalid signature" }); } // 2. Parse event const event = JSON.parse(req.body.toString()); // 3. Return 200 immediately (Attio expects fast response) res.status(200).json({ received: true }); // 4. Process asynchronously try { await processEvent(event); } catch (err) { console.error("Event processing failed:", event.event_type, err); } } ); function verifyAttioWebhook( rawBody: Buffer, signature: string, timestamp: string ): boolean { const secret = process.env.ATTIO_WEBHOOK_SECRET!; // Reject timestamps older than 5 minutes (replay protection) const age = Date.now() - parseInt(timestamp) * 1000; if (age > 300_000) { console.error("Webhook timestamp too old:", age, "ms"); return false; } // Compute expected signature const payload = `${timestamp}.${rawBody.toString()}`; const expected = crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); // Timing-safe comparison try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } catch { return false; } }
Step 4: Event Handler with Type Routing
interface AttioWebhookEvent { event_type: string; id: { event_id: string }; created_at: string; actor: { type: string; id: string }; object?: { id: { object_id: string }; api_slug: string }; record?: { id: { record_id: string } }; list?: { id: { list_id: string }; api_slug: string }; entry?: { id: { entry_id: string } }; // Additional fields vary by event type } type EventHandler = (event: AttioWebhookEvent) => Promise<void>; const handlers: Record<string, EventHandler> = { "record.created": async (event) => { const objectSlug = event.object?.api_slug; const recordId = event.record?.id?.record_id; console.log(`New ${objectSlug} record: ${recordId}`); if (objectSlug === "people") { // Fetch full record details const person = await client.get( `/objects/people/records/${recordId}` ); await syncPersonToExternalSystem(person); } }, "record.updated": async (event) => { console.log(`Updated ${event.object?.api_slug}: ${event.record?.id?.record_id}`); // Re-sync updated record }, "record.deleted": async (event) => { console.log(`Deleted record: ${event.record?.id?.record_id}`); // Remove from external system }, "record.merged": async (event) => { console.log(`Records merged into: ${event.record?.id?.record_id}`); // Update references in external system }, "list-entry.created": async (event) => { console.log(`New entry in ${event.list?.api_slug}: ${event.entry?.id?.entry_id}`); // Trigger pipeline automation }, "note.created": async (event) => { console.log("New note created"); // Sync to external note system }, "task.created": async (event) => { console.log("New task created"); // Create corresponding task in project management tool }, }; async function processEvent(event: AttioWebhookEvent): Promise<void> { const handler = handlers[event.event_type]; if (!handler) { console.log(`Unhandled event type: ${event.event_type}`); return; } await handler(event); }
Step 5: Idempotent Event Processing
// Deduplicate events using a Set, Redis, or database const processedEvents = new Set<string>(); async function processEventIdempotently(event: AttioWebhookEvent): Promise<void> { const eventId = event.id.event_id; if (processedEvents.has(eventId)) { console.log(`Duplicate event skipped: ${eventId}`); return; } await processEvent(event); processedEvents.add(eventId); // Clean up old entries (keep last 10,000) if (processedEvents.size > 10_000) { const entries = Array.from(processedEvents); entries.slice(0, entries.length - 10_000).forEach((id) => processedEvents.delete(id)); } } // For production, use Redis: // await redis.set(`attio:event:${eventId}`, "1", "EX", 86400 * 7);
Step 6: Local Webhook Testing
# Expose local server with ngrok ngrok http 3000 # Register ngrok URL as webhook target curl -X POST https://api.attio.com/v2/webhooks \ -H "Authorization: Bearer ${ATTIO_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "target_url": "https://abc123.ngrok.io/api/webhooks/attio", "subscriptions": [{"event_type": "record.created"}] }' # Create a test record to trigger the webhook curl -X POST https://api.attio.com/v2/objects/people/records \ -H "Authorization: Bearer ${ATTIO_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "data": { "values": { "email_addresses": ["webhook-test@example.com"], "name": [{"first_name": "Test", "last_name": "Webhook"}] } } }' # Remember to delete the test webhook when done
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Signature mismatch | Wrong secret or body parsed before verification | Use , verify raw body |
| Duplicate events | No idempotency | Track event IDs in Redis/DB |
| Missed events | Handler returns non-200 | Return 200 immediately, process async |
| Too many events | No filtering | Add filters to webhook subscriptions |
| Webhook deleted | Attio cleanup or token revoked | Re-register webhook, monitor with health check |
Resources
Next Steps
For performance optimization, see
attio-performance-tuning.