Claude-code-plugins-plus-skills attio-webhooks-events
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/attio-pack/skills/attio-webhooks-events" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-attio-webhooks-events && rm -rf "$T"
manifest:
plugins/saas-packs/attio-pack/skills/attio-webhooks-events/SKILL.mdsource content
Attio Webhooks & Events
Overview
Attio v2 webhooks deliver real-time CRM event notifications to your HTTPS endpoint. Subscribe to record, list-entry, note, and task events with optional object or attribute filters to reduce volume. Webhooks are managed via
POST /v2/webhooks and verified with HMAC-SHA256 signatures using a timestamp-prefixed payload.
Webhook Registration
const webhook = await fetch("https://api.attio.com/v2/webhooks", { method: "POST", headers: { "Authorization": `Bearer ${process.env.ATTIO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ target_url: "https://yourapp.com/webhooks/attio", subscriptions: [ { event_type: "record.created" }, { event_type: "record.updated", filter: { object: { $eq: "deals" } } }, { event_type: "note.created" }, { event_type: "task.completed" }, ], }), });
Signature Verification
import crypto from "crypto"; import { Request, Response, NextFunction } from "express"; function verifyAttioSignature(req: Request, res: Response, next: NextFunction) { const signature = req.headers["x-attio-signature"] as string; const timestamp = req.headers["x-attio-timestamp"] as string; const age = Date.now() - parseInt(timestamp) * 1000; if (age > 300_000) return res.status(401).json({ error: "Timestamp too old" }); const payload = `${timestamp}.${req.body.toString()}`; const expected = crypto.createHmac("sha256", process.env.ATTIO_WEBHOOK_SECRET!) .update(payload).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(401).json({ error: "Invalid signature" }); } next(); }
Event Handler
import express from "express"; const app = express(); app.post("/webhooks/attio", express.raw({ type: "application/json" }), verifyAttioSignature, (req, res) => { const event = JSON.parse(req.body.toString()); res.status(200).json({ received: true }); switch (event.event_type) { case "record.created": syncRecordToCRM(event.object?.api_slug, event.record?.id?.record_id); break; case "record.updated": reindexRecord(event.object?.api_slug, event.record?.id?.record_id); break; case "note.created": forwardToNotionSync(event.id.event_id); break; case "task.completed": closeProjectTask(event.id.event_id); break; } });
Event Types
| Event | Payload Fields | Use Case |
|---|---|---|
| , , | Sync new contacts/deals to external CRM |
| , , | Re-index changed records |
| , , | Forward meeting notes to Notion |
| , , | Close linked project management tasks |
| , | Trigger pipeline stage automation |
Retry & Idempotency
const processed = new Set<string>(); async function handleIdempotent(event: { id: { event_id: string }; event_type: string }) { const eventId = event.id.event_id; if (processed.has(eventId)) return; await routeEvent(event); processed.add(eventId); if (processed.size > 10_000) { const entries = Array.from(processed); entries.slice(0, entries.length - 10_000).forEach((id) => processed.delete(id)); } }
Error Handling
| Issue | Cause | Fix |
|---|---|---|
| Signature mismatch | Body parsed before raw verification | Use , verify raw body |
| Duplicate events | Attio retry on timeout | Track in Redis or DB |
| Missed events | Handler returns non-200 | Return 200 immediately, process async |
| Too many events | No subscription filtering | Add clauses to subscriptions |
Resources
Next Steps
See
attio-security-basics.