For developers wishing to build integrations with realtime updates, Attio supports Webhooks. Webhooks allow you to subscribe to changes that happen in Attio and then receive realtime HTTP requests to a chosen target URL to notify you of these changes.
This pattern can be useful in a wide range of contexts, but is often implemented by those who want to build realtime data syncs (e.g. an ETL pipeline) or fire automations in a timely manner (e.g. Attio’s own Zapier integration is powered by the Webhooks).
There are two places you can create Webhooks: the in-app settings UI and the API.
We offer a range of endpoints for creating, updating, deleting and viewing Webhooks. You can find these endpoints in the Webhooks section of our endpoint docs.
Creating Webhooks over the API is essential for those building integrations for Attio that will operate for many customers.
The Webhook APIs also allow you to utilise our powerful filtering functionality (see “Filtering” section below).
You can create Webhooks underneath an integration in the developer settings page.
Please note that Webhooks created with tokens that were created through our OAuth sign up flow will not be shown in the developer settings page.
When receiving Webhooks it can be hard to know if the Webhook you received was sent by us. To resolve this we attach a cryptographic signature to each webhook that we send so you can verify the origin of the request.
Attio includes two HTTP headers called X-Attio-Signature
and Attio-Signature
.
The contents of both headers is always identical - the variations are provided to help traverse different HTTP server setups.
The Attio-Signature
is calculated using a SHA256 HMAC of the request body using the Webhook secret as the secret. The Webhook's secret is viewable inside the developer settings page and in the create Webhook API response.
We encode the Attio-Signature
as a hexadecimal string.
We only sign the request body - which we interpret as a UTF-8 string.
Examples of how to generate the signature are provided below.
echo '{"workspace_id": "1ec09c5b-8375-481a-b5cb-8118ce2f1523"}' | openssl dgst -sha256 -hmac aa5c71134eb8e99f97e80785ef4d1dc22c6c5565c4d86a68ca7adf3867ab9447
Webhooks must target URLs secured with HTTPS. This is because targeting URLs using HTTP reveals the confidential content of webhooks to the public internet. In addition to exposing your data to 'man in the middle' attacks, webhooks delivered via HTTP can be read at any point in the journey to your server, for example, by cloud providers or logging services. Ensuring your target URL is encrypted with HTTPS keeps your data secure.
Webhooks guarantee at-least-once message delivery, occasionally sending duplicate messages. To deduplicate messages, Attio includes an Idempotency-Key
header which will be different for each message and the same between retries.
However, if you're receiving many duplicate messages, it may mean you're not acknowledging messages. Accepted HTTP response codes are within the 200-299 range (for example 200 or 202). If you answer with any other code, Attio will retry delivery of the message up to 10 times with an exponential back-off, which will happen over approximately 3 days in total; after which, the Webhook will be marked as degraded and we'll send you an email.
We recommend using the Request Signature to validate the request instead of relying on IP allowlisting.
This will mean that your integration does not require maintenance if we add new IP Addresses.
Attio delivers Webhooks from a fixed set of IP Addresses.
In some environments with restrictive firewalls it might be necessary to allowlist these IPs.
From time to time we might need to add a new IP Address to our list.
We'll endeavour to provide you with as much notice as possible before we do.
34.76.181.69
35.189.212.204
35.190.200.137
104.199.25.43
35.205.134.181
34.77.170.251
104.155.38.31
35.240.20.227
35.205.218.25
34.77.63.171
35.195.180.236
104.199.20.44
34.78.73.25
34.77.104.7
35.205.250.54
34.78.179.95
35.189.210.201
34.77.106.144
104.155.115.39
34.78.11.169
35.241.187.180
35.240.124.129
35.241.222.75
35.195.62.68
To avoid overwhelming your server with a large burst of requests, Attio smoothes out Webhook delivery with a rate limiter.
We rate limit delivery to a max of 25 requests per second. Please contact support if you would like this number adjusted for your Workspace.
Rate limiting is implemented on a per-target URL basis.
The developer settings page provides the ability to deliver test payloads to your Webhook's target URL.
When building an integration that uses Webhooks, this lets you quickly test that your integration is functioning, without having to modify real data in your Workspace.
We populate test payloads with real data from your Workspace. For example, when testing the `note.created` event, we'll set the `note_id` property on the payload to correspond to a real note you have created. In cases where this is not possible, for example if you have no notes in your system, we'll fallback to randomly generated fake data.
Please note that filters are not taken into account when generating test data.
To ensure your server only has to respond to events that you care about, Attio provides the ability to pass a filter query to Webhook Subscriptions. This reduces the performance constraints on your system and can help save writing additional logic on your server to determine whether or not you need to respond to events we send you.
For example, you might only care about updates to a particular Attribute on a particular List, or about new notes on People but not Companies.
Filter queries work by taking the payload of your delivered Webhook event and applying it against a simple query language to see if it passes.
Our API will validate that the filter syntax you have provided is valid.
Filters are currently only editable and viewable over the API.
{ "$and": [ { "field": "id.list_id", "value": "001f128d-2f0b-4731-9179-16d1117d9a9c", "operator": "equals" }, { "field": "id.attribute_id", "value": "41edb68d-05df-4ae8-aaca-1cbebe1393e9", "operator": "equals" } ] }
The filter syntax can be broken down into the following components.
$and
, $or
)$and
filter passes when all operations match the payload.$or
filter passes when at least one operation matches the payload.field
: Specifies which property of the webhook payload to apply the filter condition on. It supports nested properties using dot notation, such as "actor.type"
and "actor.id"
.operator
: The operator property defines the comparison operation to be used in the filter operation. The currently supported operators are:"equals"
"not_equals"
value
: The value property specifies the value to compare against the chosen payload field using the operator
.Setting a subscription filter to null
allows all events for that type through.
{ "$or": [ { "field": "id.list_id", "operator": "equals", "value": "2a33abd4-dae7-49d0-b6ed-b09da0d8f00b" // <-- Sales List ID }, { "field": "id.list_id", "operator": "equals", "value": "9d74e5c9-41eb-4d5c-b70b-d346ef15e13e" // <-- Hiring List ID } ] }
{ "$and": [ { "field": "id.list_id", "operator": "equals", "value": "2a33abd4-dae7-49d0-b6ed-b09da0d8f00b" // <-- Sales List ID }, { "field": "id.attribute_id", "operator": "equals", "value": "c65a3828-b5e9-46d9-afe6-c8319ae46412" // <-- Status Attribute ID } ] }
{ "$and": [{ "field": "actor.type", "operator": "equals", "value": "workspace-member" }] }
{ filter: null }
Webhooks were supported over the V1 API and have now been replaced by updated V2 Webhooks. Using V2 Webhooks will allow you to use our new filtering system and receive payloads which are consistent with the rest of the V2 API (e.g. we now refer to “Lists” instead of “Collections”).
V1 Webhook endpoints and even types will eventually be removed. Therefore, we recommend upgrading to use V2 Webhooks at your soonest convenience.
The following V1 Webhook events should be considered deprecated:
entry.created
entry-attribute.updated
entry.deleted
These have been replaced by the following V2 event types which fire under exactly the same circumstances.
entry.created
→ list-entry.created
entry-attribute.updated
→ list-entry.updated
entry.deleted
→ list-entry.deleted
Your code will also need to take into account the changes in the payloads of the above events.
Below are examples of payloads with V1 events and V2.
entry.created
// V1 { "event_type": "entry.created", "collection_id": "69815e80-949c-44c9-92be-242457a4be28", "entry_id": "861c1071-54ba-4d3d-b642-f72f7bcc8c7e" } // V2 { "event_type": "list-entry.created", "id": { "workspace_id": "928e88d9-de10-4e1c-9aef-36b07cb4260d", // New "list_id": "69815e80-949c-44c9-92be-242457a4be28", // Previously, colleciton_id "entry_id": "861c1071-54ba-4d3d-b642-f72f7bcc8c7e", // Previously, entry_id }, "parent_object_id": "7298c9b4-63ac-4b7e-8a74-4468d2e403a9", // New "parent_record_id": "6003a6aa-7122-45f1-b840-efe9231dfd06", // New }
entry-attribute.updated
// V1 { "event_type": "entry-attribute.updated", "collection_id": "69815e80-949c-44c9-92be-242457a4be28", "entry_id": "861c1071-54ba-4d3d-b642-f72f7bcc8c7e", "attribute_id": "18b7bb8c-fc41-4b70-be0b-0dea00b3ca23" } // V2 { "event_type": "list-entry.updated", "id": { "workspace_id": "928e88d9-de10-4e1c-9aef-36b07cb4260d", // New "list_id": "69815e80-949c-44c9-92be-242457a4be28", // Previously, colleciton_id "entry_id": "861c1071-54ba-4d3d-b642-f72f7bcc8c7e", // Previously, entry_id "attribute_id": "18b7bb8c-fc41-4b70-be0b-0dea00b3ca23", // Previously, attribute_id }, "parent_object_id": "7298c9b4-63ac-4b7e-8a74-4468d2e403a9", // New "parent_record_id": "6003a6aa-7122-45f1-b840-efe9231dfd06", // New }
entry.deleted
// V1 { "event_type": "entry.deleted", "collection_id": "69815e80-949c-44c9-92be-242457a4be28", "entry_id": "861c1071-54ba-4d3d-b642-f72f7bcc8c7e" } // V2 { "event_type": "list-entry.deleted", "id": { "workspace_id": "928e88d9-de10-4e1c-9aef-36b07cb4260d", // New "list_id": "69815e80-949c-44c9-92be-242457a4be28", // Previously, colleciton_id "entry_id": "861c1071-54ba-4d3d-b642-f72f7bcc8c7e", // Previously, entry_id }, "parent_object_id": "7298c9b4-63ac-4b7e-8a74-4468d2e403a9", // New "parent_record_id": "6003a6aa-7122-45f1-b840-efe9231dfd06", // New }
The following guide assumes you are implementing a zero downtime migration. You are, of course, welcome to migrate without such a constraint.
First, update the endpoints that handle the events we send you to deal with both V1 and V2 events.
As there will be a brief overlap period where you receive both V1 and V2 events, you may wish to make your handler idempotent.
Create new subscriptions to replace your old ones. For example, if you previously had a subscription on the "entry.created"
event type, add a new one for the "list-entry.updated"
event type.
V1 subscriptions used a static "collection_id"
property to limit subscriptions to a particular List (formerly “Collection”). This functionality can be replaced using our new filter functionality.
For example, below is an example of a V1 subscription and its V2 replacement. These two subscriptions will respond to exactly the same changes in the system.
// V1 { "event_type": "entry.created", "collection_id": "738eefb5-d481-4aed-9735-ce918f279b74" } // V2 { "event_type": "list-entry.created", "filter": { "$and": [ { "field": "id.list_id", "operator": "equals", "value": "d0a22439-5668-468a-b82a-f5988d9826f8" } ] } }
Any automated subscription creation using V1 APIs should be moved over to use V2 APIs. You should also move delete and update endpoints over to the V2 endpoints.
Your server should now be receiving both V1 and V2 events and responding to each correctly. Now that this is the case, you can go ahead and remove the V1 events using either the developer settings UI or the V1 delete endpoint.
Now that you are no longer receiving V1 events, you are welcome to clean up any code on your servers that handled the V1 events.