1. Using webhooks

    Using webhooks

    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).

    Creating Webhooks

    There are two places you can create Webhooks: the in-app settings UI and the API.

    Creating Over 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).

    Creating Using Product UI

    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.

    Authenticating

    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.

    Calculating the Request Signature

    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

    HTTPS

    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.

    Idempotency

    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.

    Delivery Attempts

    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.

    IP Addresses

    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

    Delivery Rate Limiting

    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.

    Testing Webhooks

    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.

    How to Send a Test Payload

    1. Navigate to an integration the developer settings page and select an integration.
    2. Open an existing Webhook or create a new one inside your integration.
    3. Ensure your Webhook is subscribed to the events that you want to test.
    4. Open the dropdown next to the event you want to send a test payload for and select "Send test event to target URL".
    5. We'll make an HTTP request to your target URL.

    How Test Data is Calculated

    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.

    Filtering

    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"
        } 
      ]
    }
    

    Filter Syntax

    The filter syntax can be broken down into the following components.

    Logical Joins ($and, $or)

    • An $and filter passes when all operations match the payload.
    • An $or filter passes when at least one operation matches the payload.

    Operations

    • 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.

    Filter Examples

    Subscribe to Changes on the Sales List or the Hiring List

    {
      "$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
        }
      ]
    }

    Subscribe to Changes to the Value of the “Status” Attribute of the Sales List

    {
      "$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
        }
      ]
    }

    Only Subscribe to Changes Made by Workspace Members

    {
      "$and": [{
        "field": "actor.type",
        "operator": "equals",
        "value": "workspace-member"
      }]
    }

    Subscribe to all events

    {
      filter: null
    }

    Migrating from V1 Webhooks

    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.

    Moving From V1 Events to V2

    Event Types

    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.createdlist-entry.created
    • entry-attribute.updatedlist-entry.updated
    • entry.deletedlist-entry.deleted

    Payloads

    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
    }

    Step-by-Step Migration Guide

    The following guide assumes you are implementing a zero downtime migration. You are, of course, welcome to migrate without such a constraint.

    1. Update Your Webhook Server to Handle V1 and V2 Events

    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.

    2. Add New Events

    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.

    3. Remove Old Events

    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.

    4. Clean Up Any V1 Handling Code

    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.