Skip to content
Back to skills

Attio Interaction

BSecurity

Generic Attio CRM REST API v2 recipes for querying records, upserting companies/people/deals, writing notes/tasks/comments, managing lists, and handling webhooks.

  • 844 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
ai-agentsbashreactnodeapi

Works with

  • api
  • mcp

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 3 files and shows the line behind each finding

Scanned September 3, 2026

npx -y skills add desplega-ai/agent-swarm --skill attio-interaction --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Attio Interaction?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Attio Interaction
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/desplega-ai-attio-interaction/badge)](https://www.skillsdirectory.com/skills/desplega-ai-attio-interaction)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: attio-interaction
description: Generic Attio CRM REST API v2 recipes for querying records, upserting companies/people/deals, writing notes/tasks/comments, managing lists, and handling webhooks.
user-invocable: false
---

# Attio Interaction (Read + Write)

Use this skill to read and write your Attio CRM through the REST API v2. Every read or write is a direct API call; agent-swarm does not maintain a separate Attio sync.

## TL;DR

1. Resolve `ATTIO_API_KEY` from swarm config before making calls.
2. Base URL: `https://api.attio.com/v2/`
3. Use `Authorization: Bearer $ATTIO_API_KEY`, `Content-Type: application/json`, and `Accept: application/json`.
4. Prefer upsert over create for People, Companies, and Deals: `PUT /v2/objects/{slug}/records` with `matching_attribute`.
5. Rate limits: 100 reads/sec, 25 writes/sec. Pace write bursts to roughly 15-20/sec.
6. Attribute values are arrays, even for scalar values: `[{ "value": 42 }]`, never `42`.

## Authentication

```bash
ATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)

curl -sS "https://api.attio.com/v2/objects" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Accept: application/json"
```

If calls return `401`, re-fetch the key from config. If it still fails, notify the Lead; the key may need rotation. Do not retry silently.

## Core object slugs

| Object | API slug | Primary matching attribute |
|---|---|---|
| Companies | `companies` | `domains` |
| People | `people` | `email_addresses` |
| Deals | `deals` | Usually no global dedupe key; link to company/person records |

Custom objects use their configured API slug. Discover them with `GET /v2/objects`.

## Common operations

### 1. Discover objects and slugs

```bash
curl -sS "https://api.attio.com/v2/objects" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Accept: application/json" \
  | jq '.data[] | {slug: .api_slug, name: .title}'
```

### 2. Query records with filters and pagination

```bash
curl -sS -X POST "https://api.attio.com/v2/objects/companies/records/query" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "stage": { "$not_equal": "Won" }
    },
    "limit": 100,
    "offset": 0
  }' | jq '.data[] | {record_id: .id.record_id, name: .values.name[0].value}'
```

Paginate by increasing `offset` until `data` is empty. For no filter, omit the `filter` key.

### 3. Get a single record

```bash
curl -sS "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Accept: application/json" \
  | jq '.data.values'
```

### 4. Upsert a company by domain

Use `PUT` with `matching_attribute`. It creates if not found and updates if found, so it is safe to call repeatedly.

```bash
curl -sS -X PUT "https://api.attio.com/v2/objects/companies/records" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "values": {
        "name": [{ "value": "Acme Corp" }],
        "domains": [{ "domain": "acme.com" }],
        "employee_count": [{ "value": 150 }]
      }
    },
    "matching_attribute": "domains"
  }' | jq '{record_id: .data.id.record_id}'
```

### 5. Upsert a person by email

```bash
curl -sS -X PUT "https://api.attio.com/v2/objects/people/records" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "values": {
        "name": [{ "first_name": "Jane", "last_name": "Doe" }],
        "email_addresses": [{ "email_address": "jane@acme.com" }],
        "job_title": [{ "value": "CTO" }]
      }
    },
    "matching_attribute": "email_addresses"
  }' | jq '{record_id: .data.id.record_id}'
```

An upsert returns HTTP `200` when it updates an existing record and `201` when it creates one. The response body has the same shape in both cases.

### 6. Update specific attributes on an existing record

```bash
curl -sS -X PATCH "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "values": {
        "icp_score": [{ "value": 85 }],
        "icp_tier": [{ "value": "Tier 1" }]
      }
    }
  }'
```

### 7. Write a note to a record

Notes are concise free-text entries linked to a record. Use them to preserve useful interaction context, enrichment findings, or an agent result that should remain visible in the CRM.

```bash
curl -sS -X POST "https://api.attio.com/v2/notes" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "parent_object": "companies",
      "parent_record_id": "{RECORD_ID}",
      "title": "Enrichment - 2026-06-02",
      "content": "Employee count: 150. Funding stage: Series A. Tech stack: Node.js, React."
    }
  }' | jq '{note_id: .data.id.note_id}'
```

### 8. Create a task linked to a record

```bash
curl -sS "https://api.attio.com/v2/workspace_members" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Accept: application/json" \
  | jq '.data[] | {member_id: .id.workspace_member_id, name: .name, email: .email_address}'

curl -sS -X POST "https://api.attio.com/v2/tasks" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "content": "Follow up - no contact in 21 days",
      "is_completed": false,
      "assignees": [
        { "referenced_actor_type": "workspace-member", "referenced_actor_id": "{MEMBER_ID}" }
      ],
      "linked_records": [
        { "target_object": "deals", "target_record_id": "{DEAL_RECORD_ID}" }
      ]
    }
  }' | jq '{task_id: .data.id.task_id}'
```

### 9. Post a comment on a record

```bash
curl -sS -X POST "https://api.attio.com/v2/comments" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "record": { "target_object": "companies", "target_record_id": "{RECORD_ID}" },
      "content": [
        { "type": "text", "text": "Possible duplicate of acme-corp-old - please review and merge." }
      ]
    }
  }' | jq '{comment_id: .data.id.comment_id}'
```

### 10. Query a list or pipeline view

```bash
curl -sS "https://api.attio.com/v2/lists" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Accept: application/json" \
  | jq '.data[] | {list_id: .id.list_id, name: .title}'

curl -sS -X POST "https://api.attio.com/v2/lists/{LIST_ID}/entries/query" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "limit": 100, "offset": 0 }' \
  | jq '.data[] | {entry_id: .id.entry_id, record_id: .record_id}'
```

### 11. Global search across objects

```bash
curl -sS -X POST "https://api.attio.com/v2/records/search" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "acme", "limit": 10 }' \
  | jq '.data[] | {object: .object_type, record_id: .id.record_id}'
```

## Webhooks

Attio delivers webhooks at least once. Payloads contain IDs only, so always re-fetch the full record via `GET` before acting.

Key event types:

- `record.created`, `record.updated`, `record.deleted`
- `list-entry.created`, `list-entry.updated`, `list-entry.deleted`
- `note.created`
- `task.created`, `task.completed`

Webhook timeout is 5 seconds. Respond `200` immediately and do async processing in a follow-up swarm task.

## Rate limits

| Operation | Hard limit | Safe working rate |
|---|---|---|
| Reads | 100 req/sec | ~80 req/sec |
| Writes | 25 req/sec | ~15-20 req/sec |

Add `sleep 0.05` between write calls in loops. Attio does not provide a native batch endpoint for these operations.

## Operational rules

- Upsert first. Use `PUT /records` with `matching_attribute` for create-or-update. `POST /records` can create duplicates.
- Re-fetch webhook records. Webhook payloads are event hints, not full source-of-truth records.
- Values are arrays. Every attribute value must be wrapped in an array.
- No merge endpoint. Attio has no API-level record merge; dedupe agents should flag duplicates as comments or tasks for human review.
- Check config first. Fetch `ATTIO_API_KEY` via `get-config includeSecrets=true`; never hardcode it.

## Error handling

| Status | Likely cause | Action |
|---|---|---|
| 401 | API key invalid or expired | Re-fetch from config. If still failing, notify Lead for rotation. |
| 403 | Key lacks permission | Check the Attio API key's workspace permissions. |
| 404 | Wrong object slug or record ID | Re-discover slugs with `GET /v2/objects`. |
| 400 | Malformed body | Ensure attribute values are wrapped in arrays. |
| 422 | Validation or conflict error | Read the `errors` array for field-level details. |
| 429 | Rate-limited | Back off and retry after `Retry-After` if provided. |

## Worked example: stale deal reactivation

```bash
ATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)

DEALS=$(curl -sS -X POST "https://api.attio.com/v2/objects/deals/records/query" \
  -H "Authorization: Bearer $ATTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filter": {"stage": {"$not_equal": "Won"}}, "limit": 200}')

echo "$DEALS" | jq -r '.data[] | .id.record_id' | while read -r RECORD_ID; do
  RECORD=$(curl -sS "https://api.attio.com/v2/objects/deals/records/$RECORD_ID" \
    -H "Authorization: Bearer $ATTIO_API_KEY" \
    -H "Accept: application/json")
  # Compute staleness from the relevant date attribute. If stale, create a task.
  sleep 0.05
done
```

## Related references

- Official Attio REST API docs: https://developers.attio.com/reference
- Official Attio MCP overview: https://docs.attio.com/mcp/overview

Files in this skill

  • SKILL.md9.5 KB
  • config.json542 B
  • content.md9.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.