Webhook Handlers enable apps to receive incoming requests from third-party services. Unlike server functions, which are used to send requests, Webhook Handlers are designed to listen for and process external requests. They can be used to sync data into Attio or respond with on-demand data to third-party services, making them essential for integrating external systems and automating workflows.

Example: Receiving prospect updated webhook from email sequencing API

You can use a webhook handler to receive a prospect updated event from the email sequencing API and update the corresponding record in Attio. To add a webhook handler to your app;
  • create a file with the .webhook.ts suffix.
  • The file must be placed under the src/webhooks directory, and it must export default an async function.
  • The function must take an HTTP Request argument and must return an HTTP Response.
webhooks/prospect-updated.webhook.ts
import {attioFetch} from "attio/server"

type WebhookBody = {
    event: "prospect_updated"
    data: {
        prospectId: string
        prospectName: string
        prospectEmail: string
    }
}

export default async function handleProspectUpdate(req: Request) {
    const body: WebhookBody = await req.json()

    const {prospectEmail, prospectName} = body.data

    // ℹ️ Assert endpoint will update Attio record details
    // or create a new record if it doesn't exist
    try {
        await attioFetch({
            path: `/objects/people/records`,
            method: "PUT",
            queryParams: {
                matching_attributes: "email_addresses",
            },
            body: {
                data: {
                    name: {
                        full_name: prospectName,
                    },
                    email_addresses: [
                        {
                            email: prospectEmail,
                        },
                    ],
                },
            },
        })
        return new Response(null, {status: 200})
    } catch (error) {
        console.error(error)
        return new Response(null, {status: 500})
    }
}
This webhook handler calls the Assert Record endpoint to create or update a record in Attio. Whatever status code your handler returns will be passed back to the third-party service to indicate success or failure. If your handler doesn’t return a Response object, the App SDK will respond with a 500 status code by default. Defining a webhook handler isn’t enough to make it usable. You’ll need to enable it programmatically and register it with the third-party service. You can do this from any server function, but most of the time you’ll want to do it inside the connection-added event. The connection-added event runs whenever an Attio user connects to a third-party service through your app. If your event handler completes without throwing an error, the connection will be saved. If it throws, the user will be asked to authenticate again.
Be sure to test your connection-added events carefully. If something goes wrong, users won’t be able to connect to third-party services.
Now back to our example. We are going to add connection-added event handler to our app.
  • The file must live in src/events/connection-added.event.ts
  • The file must export default an async function that takes a { connection: Connection } argument.
events/connection-added.event.ts
import type {Connection} from "attio/server"
import {createWebhookHandler, updateWebhookHandler} from "attio/server"

export default async function connectionAdded({connection}: {connection: Connection}) {
    // ℹ️ The filename must match the file in src/webhooks, but without the suffix
    const handler = await createWebhookHandler({fileName: "prospect-updated"})

    const authorizationToken = connection.value

    const response = await fetch("https://emailsequencingtool.com/api/v1/webhooks", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${authorizationToken}`,
        },
        body: JSON.stringify({
            name: handler.id,
            url: handler.url,
            event: "lead.processed",
        }),
    })

    if (!response.ok) {
        // ℹ️ Because we throw an error, the connection will not be saved
        // and user will be asked to authenticate again
        throw new Error(`Failed to register webhook: ${response.statusText}`)
    }

    const webhook = await response.json()

    // ℹ️ Save the external webhook ID so we
    // can delete it when the connection is removed
    await updateWebhookHandler(handler.id, {
        externalWebhookId: webhook.webhook_id,
    })
}
And that’s it! now whenever a prospect update webhook is received, the prospect-updated webhook handler will be called.
You can use the externalWebhookId to delete the webhook from the third-party service when the connection is removed inside the connection-removed event.