Agent Skills: Attio Webhooks & Events

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/attio-webhooks-events

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/attio-pack/skills/attio-webhooks-events

Skill Files

Browse the full folder contents for attio-webhooks-events.

Download Skill

Loading file tree…

plugins/saas-packs/attio-pack/skills/attio-webhooks-events/SKILL.md

Skill Metadata

Name
attio-webhooks-events
Description
'Implement Attio v2 webhooks -- subscribe to record/list/note/task events,

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 | |-------|---------------|----------| | record.created | object.api_slug, record.record_id, actor | Sync new contacts/deals to external CRM | | record.updated | object.api_slug, record.record_id, attribute | Re-index changed records | | note.created | event_id, actor, record | Forward meeting notes to Notion | | task.completed | event_id, actor, record | Close linked project management tasks | | list-entry.created | list.api_slug, entry.entry_id | 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 express.raw(), verify raw body | | Duplicate events | Attio retry on timeout | Track event_id in Redis or DB | | Missed events | Handler returns non-200 | Return 200 immediately, process async | | Too many events | No subscription filtering | Add filter clauses to subscriptions |

Resources

Next Steps

See attio-security-basics.