Claude-code-plugins attio-common-errors
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-common-errors" ~/.claude/skills/jeremylongshore-claude-code-plugins-attio-common-errors && rm -rf "$T"
manifest:
plugins/saas-packs/attio-pack/skills/attio-common-errors/SKILL.mdsource content
Attio Common Errors
Overview
Every Attio API error returns a consistent JSON body. This skill covers the real error codes, response format, and proven solutions for each.
Attio Error Response Format
All errors from
https://api.attio.com/v2 return this structure:
{ "status_code": 429, "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Rate limit exceeded, please try again later" }
Fields:
status_code (HTTP status), type (error category), code (specific code), message (human-readable).
Error Reference
400 Bad Request -- invalid_request
invalid_request{ "status_code": 400, "type": "invalid_request_error", "code": "invalid_request", "message": "..." }
Common causes and fixes:
| Message pattern | Cause | Fix |
|---|---|---|
| Wrong type for attribute slug | Check attribute type with |
| Used history param on unsupported type | Remove for that attribute |
| Required attribute not provided | Check on attribute definition |
| Malformed filter object | Use shorthand or verbose |
Diagnostic:
# List attributes to verify types curl -s https://api.attio.com/v2/objects/people/attributes \ -H "Authorization: Bearer ${ATTIO_API_KEY}" \ | jq '.data[] | {slug: .api_slug, type: .type, required: .is_required}'
401 Unauthorized -- authentication_error
authentication_error{ "status_code": 401, "type": "authentication_error", "code": "invalid_api_key", "message": "..." }
| Cause | Fix |
|---|---|
Missing header | Add |
| Token revoked or deleted | Generate new token in Attio dashboard |
| Malformed header | Ensure format is (one space, no quotes) |
Diagnostic:
# Verify token works curl -s -o /dev/null -w "%{http_code}" \ https://api.attio.com/v2/objects \ -H "Authorization: Bearer ${ATTIO_API_KEY}" # Should return 200
403 Forbidden -- insufficient_scopes
insufficient_scopes{ "status_code": 403, "type": "authorization_error", "code": "insufficient_scopes", "message": "Token requires 'record_permission:read-write' scope" }
| Operation | Required scopes |
|---|---|
| List/get records | + |
| Create/update records | + |
| List entries | + + |
| Create/update entries | Above + |
| Create notes | + + |
| List tasks | + + + |
| Manage webhooks | |
Fix: Edit token in Settings > Developers > Access tokens, add missing scope, save. No need to regenerate.
404 Not Found -- not_found
not_found{ "status_code": 404, "type": "not_found_error", "code": "not_found", "message": "..." }
| Cause | Fix |
|---|---|
| Wrong object slug | Verify with -- use field |
| Invalid record_id | Record may have been deleted or merged |
| Wrong list slug | Verify with |
| Typo in endpoint path | Check path starts with |
409 Conflict -- conflict
conflictOccurs when creating a record with a value that conflicts with an existing unique attribute (e.g., duplicate email or domain).
Fix: Use
PUT (assert) instead of POST to upsert:
// Assert: create or update matching record await client.put("/objects/people/records", { data: { values: { email_addresses: ["existing@example.com"], name: [{ first_name: "Updated", last_name: "Name" }], }, }, });
422 Unprocessable Entity -- validation_error
validation_error| Message pattern | Cause | Fix |
|---|---|---|
| Malformed email string | Validate email format before sending |
| Not E.164 format | Prefix with country code: |
| Attribute slug does not exist | List attributes first |
| target_record_id doesn't exist | Verify record exists first |
429 Too Many Requests -- rate_limit_exceeded
rate_limit_exceeded{ "status_code": 429, "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Rate limit exceeded, please try again later" }
Attio uses a sliding window algorithm with a 10-second window. The
Retry-After response header contains a date (usually the next second).
Immediate fix:
if (res.status === 429) { const retryAfter = res.headers.get("Retry-After"); const waitMs = retryAfter ? new Date(retryAfter).getTime() - Date.now() : 1000; await new Promise((r) => setTimeout(r, Math.max(waitMs, 100))); // Retry the request }
See
attio-rate-limits for full backoff and queue patterns.
500+ Server Error
Rare, but Attio may reduce rate limits during incidents. Always implement retry for 5xx.
Check: status.attio.com
Quick Diagnostic Script
#!/bin/bash echo "=== Attio Diagnostic ===" echo -n "Auth: " curl -s -o /dev/null -w "%{http_code}" \ https://api.attio.com/v2/objects \ -H "Authorization: Bearer ${ATTIO_API_KEY}" echo "" echo -n "Status page: " curl -s https://status.attio.com/api/v2/status.json | jq -r '.status.description' echo "Objects:" curl -s https://api.attio.com/v2/objects \ -H "Authorization: Bearer ${ATTIO_API_KEY}" \ | jq -r '.data[].api_slug' 2>/dev/null || echo "FAILED"
Error Handling Pattern
import { AttioApiError } from "./client"; async function handleAttioError(err: AttioApiError): Promise<void> { switch (err.statusCode) { case 401: throw new Error("Attio auth failed -- check ATTIO_API_KEY"); case 403: throw new Error(`Missing scope: ${err.message}`); case 404: console.warn("Resource not found, may have been deleted"); break; case 409: console.warn("Conflict -- use PUT to upsert instead"); break; case 429: /* handled by retry wrapper */ break; default: throw err; } }
Resources
Next Steps
For evidence collection, see
attio-debug-bundle. For retry patterns, see attio-rate-limits.