How to Integrate Attio with Your Headless CMS
Connect Attio to your headless CMS so sales teams see the latest customer stories, partner pages, product launches, and account content inside the CRM as soon as it ships.
What is Attio?
Attio is a CRM for go-to-market teams that lets you model companies, people, deals, lists, and workflows around your sales process. Teams use it to track accounts, route leads, run outreach, and keep customer context in one place. Its API-first data model makes it a good fit for teams that want CRM records to reflect what’s happening across their website, product, and content workflows.
Why integrate Attio with a headless CMS?
Sales teams lose context when CRM data and published content live in separate systems. A customer story goes live, a partner profile gets updated, or a pricing page changes, but the account owner still has to find the link, copy details into Attio, and remember which companies match that content. That manual step breaks fast, especially when you’re publishing 10 or 50 updates a week.
Architecture overview
A typical Sanity and Attio integration starts with a publish event in Sanity Studio. A webhook fires for a specific document type, such as customerStory, partnerProfile, or productLaunch, and sends the document ID to a Sanity Function or webhook endpoint. That server-side handler uses @sanity/client and GROQ to fetch the published document from the Content Lake, including referenced data like company domain, industry, region, product names, and canonical URL. The handler then calls Attio’s REST API at https://api.attio.com/v2, usually against standard objects like companies or people. For account-level content, you can use Attio’s record assert endpoint for the companies object and match on the domains attribute, then write custom attributes such as latest_customer_story_url, content_industry, featured_products, or sanity_document_id. The result is visible to the end user in Attio: a rep opens an account record and sees the latest approved content tied to that company, without waiting for a nightly import.
Common use cases
Customer story sync
When a customer story is published in Sanity, update the matching Attio company record with the story URL, industry, region, and products mentioned.
Partner directory to CRM
Publish partner profiles from Sanity and mirror partner status, website domain, region, and program tier into Attio company records.
Account-based sales context
Tag content by segment in Sanity, then write relevant assets to Attio so reps can find the right proof points for finance, healthcare, retail, or enterprise accounts.
Launch readiness tracking
Sync product launch pages, enablement links, and release dates into Attio so sales teams know which accounts should hear about a new feature.
Step-by-step integration
- 1
Set up Attio API access
In Attio, create or choose a workspace, then generate an API key from the developer settings. Give the key access to the objects you’ll write to, such as companies, people, or a custom object. Create custom attributes before syncing, for example sanity_document_id, latest_customer_story_url, content_industry, and featured_products.
- 2
Install the Sanity client
In your webhook handler, Sanity Function, or middleware app, install @sanity/client. Attio’s REST API works with fetch in Node 18 and later, so you don’t need a separate package unless your team prefers an HTTP client.
- 3
Model the source content in Sanity Studio
Create schemas that include the fields Attio needs. For a customerStory document, include title, slug, publishedAt, company reference, company domain, industry, region, products, and status. Keep the Attio matching field, usually the company domain, required.
- 4
Create the sync trigger
Add a Sanity webhook filtered to published document types, such as _type == 'customerStory' && !(_id in path('drafts.**')). Point it to a Sanity Function or secure API route. Include the document ID in the payload.
- 5
Call Attio’s API
In the handler, fetch the document with GROQ, map Sanity fields to Attio attribute slugs, and call Attio’s record assert endpoint for the right object. For company content, match on domains so repeat publishes update the same Attio company record instead of creating duplicates.
- 6
Test the sales workflow
Publish one test customer story, confirm the webhook fired, check the Attio company record, and verify that custom attributes are filled correctly. Then test edits, missing domains, deleted content, and permission errors before sending production events.
Code example
import {createClient} from '@sanity/client'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-02-19',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false,
})
export async function POST(req: Request) {
const {documentId} = await req.json()
const story = await sanity.fetch(`*[_id == $id][0]{
title,
"slug": slug.current,
industry,
"companyName": company->name,
"domain": company->domain,
"products": products[]->name
}`, {id: documentId.replace('drafts.', '')})
if (!story?.domain) {
return Response.json({skipped: 'Missing company domain'}, {status: 200})
}
const res = await fetch('https://api.attio.com/v2/objects/companies/records', {
method: 'PUT',
headers: {
authorization: `Bearer ${process.env.ATTIO_API_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({
data: {
matching_attribute: 'domains',
values: {
domains: [story.domain],
name: story.companyName,
latest_customer_story_title: story.title,
latest_customer_story_url: `https://example.com/customers/${story.slug}`,
content_industry: story.industry,
featured_products: story.products,
sanity_document_id: documentId,
},
},
}),
})
if (!res.ok) {
return Response.json({error: await res.text()}, {status: 502})
}
return Response.json({synced: true})
}How Sanity + Attio works
Build your Attio integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect published content with Attio records.
Start building free →CMS approaches to Attio
| Capability | Traditional CMS | Sanity |
|---|---|---|
| CRM-ready content structure | Content often lives inside pages, so teams may need manual copy, exports, or HTML parsing before writing to Attio. | The AI Content Operating System structures content as typed JSON in the Content Lake, with references that GROQ can join in one query. |
| Real-time sync on publish | Sales teams often wait for manual updates, scheduled exports, or nightly jobs. | Webhooks trigger on publish events, and Functions can run the Attio write without separate infrastructure for common sync jobs. |
| Field-level control for Attio writes | Exports can include too much page data, too little metadata, or fields that don’t match CRM attributes. | GROQ selects exact fields, such as domain, industry, products, URL, and publish date, then maps them to Attio attribute slugs. |
| Sales context from content references | Related products, regions, authors, and customer data may be embedded in page layout or plugin data. | A single GROQ query can fetch a story plus referenced company, product, region, and campaign data for one Attio update. |
| AI agent access to the same content | Agents often need a separate index built from rendered pages or exports. | Agent Context gives production AI agents read-only, scoped access to structured content that also feeds Attio and your site. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Salesforce
Sync structured content, campaign assets, and account metadata from Sanity into Salesforce workflows.
Sanity + HubSpot CRM
Connect published content with HubSpot contacts, companies, and campaigns for sales and marketing follow-up.
Sanity + Close
Send approved sales content and account-specific page links from Sanity into Close for outbound teams.