Skillshub attio-common-errors

install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/attio-common-errors" ~/.claude/skills/comeonoliver-skillshub-attio-common-errors && rm -rf "$T"
manifest: skills/jeremylongshore/claude-code-plugins-plus-skills/attio-common-errors/SKILL.md
source 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

{ "status_code": 400, "type": "invalid_request_error", "code": "invalid_request", "message": "..." }

Common causes and fixes:

Message patternCauseFix
Invalid value for attribute
Wrong type for attribute slugCheck attribute type with
GET /v2/objects/{obj}/attributes
Cannot query historic values
Used history param on unsupported typeRemove
show_historic
for that attribute
Missing required field
Required attribute not providedCheck
is_required
on attribute definition
Invalid filter format
Malformed filter objectUse shorthand
{ "email": "x" }
or verbose
{ "$and": [...] }

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

{ "status_code": 401, "type": "authentication_error", "code": "invalid_api_key", "message": "..." }
CauseFix
Missing
Authorization
header
Add
Authorization: Bearer sk_...
Token revoked or deletedGenerate new token in Attio dashboard
Malformed headerEnsure format is
Bearer <token>
(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

{ "status_code": 403, "type": "authorization_error", "code": "insufficient_scopes",
  "message": "Token requires 'record_permission:read-write' scope" }
OperationRequired scopes
List/get records
object_configuration:read
+
record_permission:read
Create/update records
object_configuration:read
+
record_permission:read-write
List entries
object_configuration:read
+
record_permission:read
+
list_entry:read
Create/update entriesAbove +
list_entry:read-write
Create notes
note:read-write
+
object_configuration:read
+
record_permission:read
List tasks
task:read
+
object_configuration:read
+
record_permission:read
+
user_management:read
Manage webhooks
webhook:read-write

Fix: Edit token in Settings > Developers > Access tokens, add missing scope, save. No need to regenerate.

404 Not Found --
not_found

{ "status_code": 404, "type": "not_found_error", "code": "not_found", "message": "..." }
CauseFix
Wrong object slugVerify with
GET /v2/objects
-- use
api_slug
field
Invalid record_idRecord may have been deleted or merged
Wrong list slugVerify with
GET /v2/lists
Typo in endpoint pathCheck path starts with
/v2/

409 Conflict --
conflict

Occurs 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

Message patternCauseFix
Invalid email address
Malformed email stringValidate email format before sending
Invalid phone number
Not E.164 formatPrefix with country code:
+14155551234
Unknown attribute
Attribute slug does not existList attributes first
Invalid record reference
target_record_id doesn't existVerify record exists first

429 Too Many Requests --
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
.