Brevo to Attio Sync
Keep Attio as the CRM source of truth and Brevo as the marketing-execution layer. This custom sync pushes Attio People contacts and their marketing-relevant attributes into Brevo without manual export work. Scoped Attio attribute changes trigger a real-time push, Brevo contacts are upserted by the chosen identifier, and the exact attribute set is agreed before the custom build is installed.
Who this is for
This build is for teams that run customer relationships in Attio and marketing execution in Brevo. It fits when Attio People attributes such as lifecycle stage, segmentation flags, consent state, or billing tier need to reach Brevo automatically.
The goal is not to mirror every CRM field. The goal is to keep Brevo contacts fresh enough for marketing work while Attio remains the place where relationship data is edited and governed.
How the sync works
The default flow is outbound from Attio to Brevo. An Attio attribute change triggers the sync, the app writes a Brevo contact through the Contacts API, and a narrow writeback keeps the Attio Person linked to the Brevo contact when the Brevo ID is available.
Webhook trigger from Attio
Setup registers an Attio webhook subscription on record.updated for People. The subscription is filtered to the scoped attribute slugs only, so changes outside the agreed marketing attribute set do not trigger a Brevo push.
That attribute-change scoping keeps the sync surface narrow. A billing-tier change can push to Brevo; an unrelated CRM note update can stay in Attio.
Brevo upsert
The app calls POST /v3/contacts with updateEnabled: true. For this build, email is the chosen identifier. Brevo also supports SMS and ext_id as identifier types, so scope decides if a workspace needs something other than email.
The same request can include listIds, which routes the contact to the configured Brevo list or lists without requiring a separate add-to-list call for the default path.
Writeback of brevo_id
When a Brevo contact is first created, Brevo documents an id in the create-contact response. On later updateEnabled: true updates, the Brevo docs do not prove that id is always returned. The app reads the response when the ID is present and falls back to GET /v3/contacts/{email}?identifierType=email_id to confirm the link when needed.
The brevo_id field on Attio People is read-only for users and written by the app actor context. That writeback is kept out of the scoped trigger set so it does not re-fire the same attribute-change webhook.
List routing
The default build sends every synced contact to one Brevo list. Multi-list membership and conditional routing are scoped when the workspace needs them.
For routing changes, the app uses listIds on the upsert for the destination list or lists. When a rule moves a contact out of a previous list, the app uses POST /v3/contacts/lists/{id}/contacts/remove for the removal step.
What we configure during scope
The scoping call turns the Attio-to-Brevo pattern into your build. These decisions are made before the app is installed.
| Decision | What we decide with you |
|---|---|
| Trigger attributes | Which Attio People attributes should fire the sync when they change. |
| Brevo destination lists | Which Brevo list or lists contacts should land on after each push. |
| Conditional routing rules | Which Attio attribute values send a contact to list A, list B, or multiple lists. |
| Attribute map | Which Brevo contact attributes receive Attio values, including category or select mirroring verified during scope per workspace. |
| Identifier strategy | Email by default, with SMS or ext_id considered when the workspace requires a different Brevo identifier. |
| Backfill scope | Which existing Attio People should be pushed once, batched through POST /v3/contacts/import for larger backfills to respect Brevo body-size limits. |
| Optional inbound flow | Phase 2 handling for Brevo opens, clicks, bounces, unsubscribes, or other email events mirrored back to Attio. |
Why this works with the Brevo API
Brevo API v3 exposes the pieces this pattern needs: API key authentication, account validation, contact upsert, contact attribute discovery, list validation, list membership changes, bulk import, and optional marketing or transactional webhooks for Phase 2.
Direct API access uses the api-key header. During setup, the app validates the credential with GET /v3/account, which returns account details when the key is accepted.
The outbound write uses POST /v3/contacts. Brevo documents updateEnabled for updating an existing contact in the same request and listIds for adding the contact to lists during create or upsert. For this build, email is the default identifier, while SMS and ext_id stay available as scope decisions.
Provisioning reads GET /v3/contacts/attributes, which returns the full list of contact attributes. Brevo does not expose a documented single-attribute read endpoint in the scoped Contacts reference, so the app reads the full attribute list and filters client-side by category and name.
Category attribute enumeration is mirrored at provisioning time. Brevo exposes category options under enumeration on the contact attributes response, including option labels. The contact upsert write value for category attributes is verified during scope per workspace because the create-contact docs do not directly say that category labels can be written as contact attribute values.
List existence is verified with GET /v3/contacts/lists/{listId}, which documents 404 when the list is missing. For larger list reads, GET /v3/contacts/lists supports limit and offset.
Phase 2 inbound work can use Brevo's marketing and transactional webhook surfaces. Brevo documents webhook creation with POST /v3/webhooks and event groups for opens, clicks, bounces, unsubscribes, delivered events, list additions, contact updates, and contact deletions.
How reliability is handled
The outbound write is idempotent at two layers. Attio-side idempotency keys prevent duplicate webhook processing. Brevo does not document an idempotency header for contact writes, but POST /v3/contacts with updateEnabled: true makes repeated writes for the same email and same attributes redundant.
Brevo error responses use an envelope with code and message. The app treats request, auth, permission, and missing-resource failures as configuration or data errors, not retry loops.
For 429 responses, Brevo documents rate-limit headers. Retry handling reads x-sib-ratelimit-reset and waits for the reset window instead of retrying tightly.
Brevo API rate limits and backfill pacing
Brevo contacts endpoint limits are tier-specific. General contacts endpoints are limited to 10 RPS and 36,000 RPH. Advanced contacts endpoints are limited to 20 RPS and 72,000 RPH. Extended contacts endpoints are limited to 60 RPS.
Brevo sends x-sib-ratelimit-limit, x-sib-ratelimit-remaining, and x-sib-ratelimit-reset. The sync uses those headers to pace retries and to keep live pushes from fighting a backfill.
Larger backfills use POST /v3/contacts/import. Brevo documents a 10 MB maximum for fileBody and jsonBody, with about 8 MB recommended to avoid parsing-size issues.
How setup works
Setup separates business scope from API setup. Scope decides what should move. Provisioning validates the Brevo account, creates or refreshes the Attio surfaces, and registers the narrow webhook that starts the real-time push.
- Scope the Brevo and Attio model. We confirm the Attio People attributes that should trigger the sync, the Brevo list routing model, the attribute map, the identifier strategy, and whether any inbound Brevo event flow belongs in Phase 2.
- Generate Brevo v3 API key. You create a Brevo v3 API key from Brevo account settings. The app validates the key with GET /v3/account before provisioning continues.
- Install the Attio app. We install the custom Attio app into your workspace. Workspace Settings exposes one Brevo API key field and a Re-provision button for controlled schema refreshes.
- Provisioning runs automatically. The app creates the brevo_id attribute on Attio People, mirrors agreed Brevo category enum labels into Attio selects, registers the Attio webhook with the scoped attribute filter, and verifies that the target Brevo list or lists exist.
- Real-time sync begins. Every scoped attribute change fires an Attio webhook, the contact is upserted in Brevo within seconds, and the Push to Brevo bulk action remains available for smaller backfills and one-off re-pushes.
Known limitations
Custom app, not a marketplace install
This is a scoped custom engagement. It is not a public Brevo marketplace app, not an Attio Marketplace app, and not a one-click install.
One-way Attio to Brevo by default
The standard build pushes Attio People changes to Brevo. Brevo event ingestion for opens, clicks, bounces, unsubscribes, and related events is a Phase 2 scope decision, not bundled into the default flow.
Email is the default identifier
Email is the chosen identifier for the standard build. Changing an Attio Person's email can create a new Brevo contact at the new email; the old Brevo contact remains unless a merge or cleanup step is scoped.
Category attribute writes are mapped, not auto-mirrored at write time
Brevo category option labels are mirrored into Attio selects during provisioning. If Brevo enum options change after launch, use Re-provision to refresh the Attio options. Contact write value semantics for category attributes are verified during scope per workspace.
No documented single-attribute read endpoint
Brevo's scoped Contacts reference documents GET /v3/contacts/attributes for listing attributes. It does not document a single-attribute read endpoint, so provisioning reads the full attribute list once per re-provision and filters client-side.
Bulk re-push is capped at 250 in Attio
The 250-record cap belongs to the Attio bulk-action UI, not Brevo. Larger backfills use POST /v3/contacts/import and are paced around Brevo's 8 MB recommended body size, 10 MB maximum body size, and tier-specific rate limits.
Brevo does not document an idempotency header for contact writes
The build relies on Attio-side delivery deduplication and Brevo upsert-by-email behavior. Repeating the same email and same attributes is a redundant safe write, but it is not backed by a Brevo idempotency header.
Schema changes need controlled reprovisioning
New Brevo attributes, changed category options, changed Attio attributes, or changed trigger rules need a Re-provision pass. Re-provision re-reads Brevo attributes, refreshes enum mirrors, verifies list configuration, and updates the webhook attribute filter.
Frequently asked questions
Is this a marketplace app or custom engagement?
This is a scoped custom engagement, not a public marketplace app or a one-click install. The app pattern is client-proven, but the attribute map, list routing, identifier strategy, and optional inbound flow are decided per workspace.
What direction does data flow?
The default build is one-way Attio to Brevo. Attio is the CRM source of truth, and Brevo receives the marketing-relevant contact data. Brevo marketing or transactional event ingestion can be scoped as Phase 2.
How are Attio People matched to Brevo contacts?
Email is the chosen identifier for this build. Brevo also supports SMS and ext_id as contact identifiers, so scope decides if a workspace needs a different strategy. For the standard build, POST /v3/contacts uses updateEnabled: true with email.
How fast does a change in Attio reach Brevo?
The sync runs from an Attio record.updated webhook filtered to the scoped People attributes. In normal operation, the Brevo upsert runs within seconds after the Attio attribute change is delivered.
What happens if a webhook is delivered twice?
Attio-side idempotency keys prevent duplicate processing. Brevo does not document an idempotency header for contact writes, but an upsert by the same email with the same attributes is naturally a redundant write.
Can the integration handle multiple Brevo lists?
Yes. Multi-list routing is decided during scope. The app can send listIds on the contact upsert and can remove contacts from previous lists with POST /v3/contacts/lists/{listId}/contacts/remove when a move is part of the routing rule.
How are Brevo category attributes kept in sync with Attio selects?
During provisioning, the app reads GET /v3/contacts/attributes, finds the agreed category attributes, and mirrors their enumeration labels into Attio selects. If Brevo enum options change later, Re-provision refreshes the Attio options.
What about Brevo rate limits?
Brevo documents contacts endpoint limits by tier: General is 10 RPS and 36,000 RPH, Advanced is 20 RPS and 72,000 RPH, and Extended is 60 RPS. The app reads x-sib-ratelimit-limit, x-sib-ratelimit-remaining, and x-sib-ratelimit-reset and backs off after 429 responses.
Does the integration backfill existing Attio People on first install?
Not automatically. Smaller backfills can use the Push to Brevo bulk action in Attio, which is capped by the Attio UI selection size. Larger backfills use POST /v3/contacts/import and are paced around Brevo body-size and rate-limit constraints.
What do we need on the Brevo side?
You need a Brevo account, a v3 API key, and the target Brevo list or lists created before setup. The app validates the API key with GET /v3/account and verifies list existence with GET /v3/contacts/lists/{listId}.
API sources checked
- Brevo API key concepts
- Brevo API key authentication
- Brevo create and manage API keys
- Brevo Get account details
- Brevo Create a contact
- Brevo Update a contact
- Brevo Get a contact's details
- Brevo Get all the contacts
- Brevo List all attributes
- Brevo Create contact attribute
- Brevo Update contact attribute
- Brevo Delete an attribute
- Brevo Get all the lists
- Brevo Get a list's details
- Brevo Add existing contacts to a list
- Brevo Delete a contact from a list
- Brevo Import contacts
- Brevo API rate limits
- Brevo rate limit headers
- Brevo platform quotas
- Brevo getting started with webhooks
- Brevo webhook retry mechanism
- Brevo secure webhook calls
- Brevo Create a webhook
- Brevo marketing webhooks
- Brevo transactional webhooks
- Brevo SDKs and libraries
- Brevo IP security and authorization
- Attio Upsert a Record
- Attio App SDK record actions
- Attio App SDK bulk record actions