Attio

12 minutes

Attio API: complete guide for developers

The Attio API allows you to connect the CRM to a SaaS product, a billing tool, an enrichment platform, or an internal system. It directly reflects Attio's flexible model: standard or custom objects, records, lists, entries, attributes, notes, and tasks. However, not all customizations require an external integration. Attio also offers Workflows, a JavaScript block, HTTP requests, and an App SDK. This guide explains how to choose the right level, and then build a reliable integration.

Nadir BOUSSETTA

Updated on

LinkedIn

What the Attio API enables

Attio provides a public REST API that exchanges JSON over HTTPS. The current endpoints are exposed under /v2/ and notably allow you to:

  • read and modify workspace objects;

  • create, search, and update records;

  • manage list entries and pipelines;

  • manipulate attributes, notes, tasks, and comments;

  • create webhooks;

  • connect an application using OAuth 2.0.

The unique feature of Attio is that the API is not limited to people, companies, and deals. A custom object created in the workspace can be queried and modified using the same patterns as standard objects.

The official documentation remains the source of truth for endpoints and schemas. Attio also publishes an OpenAPI specification to explore the API or generate part of the types and clients needed for an integration.

Access token or OAuth 2.0?

The first choice does not concern the language, but the authentication method.

Situation

Recommended method

Internal script for a single workspace

Workspace access token

Private synchronization with your product

Dedicated access token

Application used by multiple Attio customers

OAuth 2.0

Application distributed in the Attio ecosystem

OAuth 2.0 and developer platform

For a single workspace, an administrator can create an access token from the developer settings. For a multi-workspace application, Attio recommends OAuth 2.0 so that each customer authorizes their own space.

In both cases, the token is sent in the Authorization header:




Tokens use scopes. An integration that only reads records must not have write permissions.

Some simple rules:

  • create one token per integration and per environment;

  • give it an explicit name;

  • store it in a secrets manager;

  • never expose it in a browser or a Git repository;

  • revoke it as soon as it is no longer useful.

Understanding the Attio data model

The main difficulty is generally not the HTTP call. It consists of understanding what you are manipulating.

Objects and records

An object defines a type of entity: People, Companies, Deals, or a custom object like Contracts or Subscriptions.

A record is an instance of this object. Jeanne Dupont is a record of People; Acme SAS is a record of Companies.

Lists and entries

A list represents a process or grouping: sales pipeline, renewal portfolio, or partner tracking.

When a record is added to a list, Attio creates an entry. The record carries the lasting data of the entity; the entry carries the data specific to the process.

For example:

  • the domain and sector belong to the Company record;

  • the stage, amount, or closing date can belong to its entry in a pipeline.

A single record can therefore participate in multiple processes without being duplicated.

Attributes

Attributes are the fields of objects and lists. Attio supports various types: text, number, date, status, select, email, phone, relationship to another record, and other structured formats.

The payload format depends on the attribute type. It is therefore important not to assume that a complex value can always be sent as a simple string.

Notes and tasks

The API also exposes notes and tasks. An integration can create a report on the correct account, generate a follow-up, or attach a task to multiple records.

Find the general product operation in our Attio features guide.

Creating or updating a record

Creating a person

Creation uses POST /v2/objects/{object}/records.

curl --request PUT \
  --url "https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "values": {
        "email_addresses": ["jeanne@exemple.fr"],
        "name": [
          {
            "first_name": "Jeanne",
            "last_name": "Dupont",
            "full_name": "Jeanne Dupont"
          }
        ]
      }
    }
  }'
curl --request PUT \
  --url "https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "values": {
        "email_addresses": ["jeanne@exemple.fr"],
        "name": [
          {
            "first_name": "Jeanne",
            "last_name": "Dupont",
            "full_name": "Jeanne Dupont"
          }
        ]
      }
    }
  }'
curl --request PUT \
  --url "https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "values": {
        "email_addresses": ["jeanne@exemple.fr"],
        "name": [
          {
            "first_name": "Jeanne",
            "last_name": "Dupont",
            "full_name": "Jeanne Dupont"
          }
        ]
      }
    }
  }'

If an attribute marked as unique conflicts with an existing record, the creation fails. For synchronization, use an upsert instead.

Using upsert to avoid duplicates

The upsert creates the record if it does not exist and updates it if it already exists. It uses a unique attribute specified by the matching_attribute parameter. For People, the email address is the natural choice.

curl --request PUT \
  --url "https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "values": {
        "email_addresses": ["jeanne@exemple.fr"],
        "name": [
          {
            "first_name": "Jeanne",
            "last_name": "Dupont",
            "full_name": "Jeanne Dupont"
          }
        ]
      }
    }
  }'
curl --request PUT \
  --url "https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "values": {
        "email_addresses": ["jeanne@exemple.fr"],
        "name": [
          {
            "first_name": "Jeanne",
            "last_name": "Dupont",
            "full_name": "Jeanne Dupont"
          }
        ]
      }
    }
  }'
curl --request PUT \
  --url "https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "values": {
        "email_addresses": ["jeanne@exemple.fr"],
        "name": [
          {
            "first_name": "Jeanne",
            "last_name": "Dupont",
            "full_name": "Jeanne Dupont"
          }
        ]
      }
    }
  }'

For a custom object, first define a suitable unique attribute: customer ID, contract reference, or identifier from the source system.

Updating with PATCH

PATCH /v2/objects/{object}/records/{record_id} modifies the provided values.

Watch out for multiselect attributes: a PATCH update adds new values to existing values. Use PUT if you need to completely replace the collection.

Searching and paginating records

Records are filtered with a POST on the /query endpoint:

curl --request POST \
  --url "https://api.attio.com/v2/objects/companies/records/query" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "filter": {
      "name": "Acme"
    },
    "limit": 50,
    "offset": 0
  }'
curl --request POST \
  --url "https://api.attio.com/v2/objects/companies/records/query" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "filter": {
      "name": "Acme"
    },
    "limit": 50,
    "offset": 0
  }'
curl --request POST \
  --url "https://api.attio.com/v2/objects/companies/records/query" \
  --header "Authorization: Bearer $ATTIO_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "filter": {
      "name": "Acme"
    },
    "limit": 50,
    "offset": 0
  }'

The available syntaxes depend on the attribute type. Test filters with the official reference rather than copying a syntax designed for another field.

Some best practices:

  • filter server-side;

  • never assume that a response contains all results;

  • respect the pagination system of the endpoint;

  • use stable slugs or UUIDs rather than visible labels;

  • log Attio identifiers and those of the source system.

Depending on the resources, Attio uses pagination by limit and offset or by cursor. A generic access layer must therefore support both mechanisms.

Setting up reliable webhooks

Webhooks allow you to react to a modification without regularly polling the API. They are suitable, for example, for:

  • launching onboarding when a deal is won;

  • synchronizing a correction made by a sales representative;

  • notifying Slack when a stage changes;

  • feeding a data warehouse continuously.

Attio signs each webhook with an HMAC SHA-256 of the raw body. The signature is transmitted in Attio-Signature, and also duplicated in X-Attio-Signature. Verify it with the webhook secret before processing the payload.

Other behaviors to anticipate:

  • the target URL must use HTTPS;

  • delivery is guaranteed at least once;

  • Idempotency-Key allows deduplication of attempts;

  • the server must respond in less than five seconds;

  • any response outside 200–299 triggers retries;

  • Attio can retry up to ten times over about three days;

  • delivery is limited by default to 25 requests per second per URL.

The correct pattern is to verify the signature, record the event, immediately respond with 202, then perform the processing in a queue.

Rate limits and error handling

Attio currently applies a global limit of:

  • 100 requests per second for reads;

  • 25 requests per second for writes.

A 429 Too Many Requests response contains a Retry-After header. Wait for this duration before retrying.

The endpoints for listing records and entries also apply a limit based on the complexity of filters, sorting, and data volume. A single overly complex request can therefore be rejected even if your rate remains low.

In production:

  • queue heavy processing;

  • limit the number of concurrent workers;

  • respect Retry-After;

  • only retry temporary errors;

  • prefer webhooks over polling;

  • simplify requests that receive a complexity error.

Do you really need to use the API to customize Attio?

Not always. Attio offers several levels of extension before having to host a complete integration.

Native workflow

Use a Workflow when the trigger and actions stay within Attio or an already connected application: assigning a lead, creating a task, updating a status, or launching a sequence.

Send HTTP request

The Send HTTP request block allows calling an external service using GET, POST, PATCH, PUT, DELETE, or HEAD. It returns the status and response body, which can then be parsed with Parse JSON.

This is sufficient to call a webhook, send a simple payload, or retrieve data from an API. The current timeout for the block is two minutes.

Execute code

The Execute code block runs a JavaScript function within a Workflow. It receives configured variables and returns a value that can be used in subsequent steps. Its current timeout is 100 seconds.

For users coming from Airtable, its role is similar to the script action of an automation. The decision logic is also similar to that presented in our Airtable API guide: embedded code is suitable for local transformations, while the direct API becomes preferable when the integration needs to be independent, large-scale, or deeply linked to a product.

Record command and List entry command

These triggers add a manual command to a record or a list entry. A user can thus trigger a Workflow from within Attio: generate a document, launch an enrichment, or send information to another system.

App SDK

The App SDK goes further. It allows adding to Attio:

  • actions and buttons on records;

  • custom widgets and interfaces;

  • server functions executed within the Attio infrastructure;

  • custom Workflow blocks;

  • incoming webhooks;

  • secure connections to external services.

The REST API remains preferable when an external system needs to synchronize data with Attio. The App SDK becomes relevant when the experience needs to live directly within the CRM interface.

API, Workflow, SDK or no-code: how to choose?

Need

Solution

Simple update in Attio

Native workflow

One-off call to an external service

Send HTTP request

Short JavaScript transformation

Execute code

Manual action from a record

Record command

Moderate cross-tool automation

Make, Zapier, or n8n

Critical or high-volume synchronization

REST API and webhooks

Application used by multiple workspaces

OAuth 2.0

Interface or function integrated into Attio

App SDK

No-code and API are not mutually exclusive. A pragmatic architecture can reserve the API for critical flows and use Make or n8n for peripheral automations.

Our Attio integrations guide presents the main options depending on your stack.

Three concrete integrations

Connecting Attio to a SaaS product

When a workspace is created in your product, the integration creates or updates the company, users, and corresponding account in Attio. Activation, upgrade, or churn events then populate CRM attributes.

Upsert and external identifiers are essential to avoid duplicates.

Triggering onboarding after the sale

When a deal moves to "won," a webhook launches the creation of the onboarding project, tasks, and notifications. The Attio Workflow can cover internal actions; the API or n8n takes over if multiple business systems are involved.

Synchronizing billing and renewals

An integration can link subscriptions, invoices, or due dates from Stripe, Pennylane, or an ERP to the concerned company. The CRM can then create a task before a renewal or flag an unpaid invoice.

Building a reliable Attio integration

A successful demo is not yet a production integration.

Before deployment:

  1. define the master system for each data point;

  2. maintain a stable external identifier;

  3. use upsert rather than blind creation;

  4. make webhook processing idempotent;

  5. separate staging and production;

  6. log requests, errors, and identifiers;

  7. add a queue for long operations;

  8. plan a periodic reconciliation;

  9. monitor degraded webhooks and 429 errors;

  10. document the recovery procedure.

Attio's flexible model does not negate the need to define clear synchronization rules. Data that can be modified in multiple systems without arbitration almost always ends up diverging.

Need help integrating Attio into your stack?

HyperOps supports B2B companies in designing and developing Attio integrations: data model, API, webhooks, Workflows, n8n, Make, and App SDK.

We start by identifying master data, volumes, and errors to handle before choosing the appropriate technical level. The goal is not to write code for the sake of it, but to build an integration that is easy to monitor and reliable over time.

Discover our services on the Attio Agency page, our expertise in automation and AI, or tell us about your project.

Frequently asked questions about the Attio API

How do I get an Attio API key?

An administrator can create an access token in the workspace's developer settings, then select the necessary scopes.

Should you use OAuth with the Attio API?

OAuth 2.0 is recommended for an application that needs to be installed on multiple workspaces. An access token is generally sufficient for an internal integration within a single workspace.

Does Attio offer webhooks?

Yes. Webhooks allow you to receive changes in real time. They are signed, delivered at least once, and must be processed idempotently.

Can you execute JavaScript directly in Attio?

Yes. The Execute code block in Workflows executes a JavaScript function and passes its output to the subsequent steps. The App SDK allows you to go further with server functions and built-in extensions.

What is the difference between the Attio API and the MCP server?

The API is used to build deterministic integrations between systems. The MCP server allows Claude, ChatGPT, or another compatible assistant to query and modify Attio using natural language. Find all the details in our guide on Attio MCP and AI.

Need to go further on this topic?

Explain to us how you operate and the difficulties you are facing. No need for specifications: a few pieces of context are enough to get started.