Generic Attio CRM REST API v2 recipes for querying records, upserting companies/people/deals, writing notes/tasks/comments, managing lists, and handling webhooks.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add desplega-ai/agent-swarm --skill attio-interaction --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Attio Interaction?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/desplega-ai-attio-interaction)More formats (shields.io, HTML) on the badges page.
---
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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!