Skip to main content

How to send Browse AI data to Attio

M
Written by Melissa Shires

This guide covers three ways to send your Browse AI scraped data into Attio, from no-code automation to custom API integrations. Choose the method that best fits your team's technical comfort and use case.

πŸ“– Prerequisites: You'll need an approved Browse AI robot with scraped data and an Attio workspace. For API-based methods, you'll also need a Browse AI API key and an Attio access token.

Which method should I use?

Here's a quick comparison to help you decide:

Method

Best for

Technical level

Speed

Zapier / Make

Teams without developers

No code

Near real-time

Webhooks

Real-time pipelines with custom logic

Intermediate

Real-time

API polling

Batch processing, scheduled syncs

Intermediate

On schedule

Method 1: Zapier or Make (no code)

The fastest way to connect Browse AI to Attio. No coding required. Just map your scraped fields to Attio objects.

Setting up with Zapier

  1. Go to zapier.com and create a new Zap.

  2. Trigger: Choose Browse AI as the trigger app, then select New Successful Task Finished as the event.

  3. Connect your Browse AI account and select the robot you want to sync data from.

  4. Action: Choose Attio as the action app, then select Create Record and choose the object type (People, Companies, or a custom object).

  5. Connect your Attio workspace.

  6. Map your fields: Match each Browse AI captured field to the corresponding Attio attribute. For example, map email β†’ Email addresses, company_name β†’ Name.

  7. Test the Zap, then turn it on.

πŸ’‘ Tip: Attio automatically deduplicates People by email address and Companies by domain. If a record already exists, you can use Attio's Assert Record action in Zapier to update it instead of creating a duplicate.

Setting up with Make (formerly Integromat)

  1. Create a new scenario in Make.

  2. Add a Webhooks β†’ Custom webhook module as the trigger. Copy the webhook URL.

  3. In Browse AI, go to your robot's Integrate tab and add the Make webhook URL under Webhooks. Select the taskFinishedSuccessfully event.

  4. Add an Attio β†’ Create a Record module (or use HTTP module with Attio's API), connect your account, and map your fields.

  5. Activate the scenario.

Method 2: Webhooks (real-time, custom code)

Use Browse AI webhooks to push data directly into Attio as soon as a task finishes. This gives you full control over data transformation and error handling.

How it works

  1. Browse AI sends a webhook POST request to your server when a task completes.

  2. Your server receives the scraped data and transforms it for Attio's REST API.

  3. Your server calls Attio's API to create or assert (upsert) records.

Step 1: Create an Attio integration and access token

  1. In Attio, go to Workspace Settings β†’ Developers.

  2. Click Create a new integration.

  3. Name it (e.g. "Browse AI Integration").

  4. Generate an access token and copy it.

⚠️ Keep your access token secure. Store it in environment variables. Never hardcode it in your source code or commit it to version control. Access tokens are scoped to a single workspace.

Step 2: Build your webhook endpoint

This example receives Browse AI webhook data and creates (or updates) a Person record in Attio. Attio's assert endpoint is ideal here as it creates a record if it doesn't exist, or returns the existing one if it matches on a unique attribute like email:

import requests
from flask import Flask, request, jsonifyapp = Flask(__name__)ATTIO_ACCESS_TOKEN = "your_attio_access_token"
ATTIO_API_URL = "https://api.attio.com/v2"def assert_person(person_data):
    """Create or find a Person record in Attio (upsert by email)."""
    resp = requests.put(
        f"{ATTIO_API_URL}/objects/people/records",
        headers={
            "Authorization": f"Bearer {ATTIO_ACCESS_TOKEN}",
            "Content-Type": "application/json"
        },
        json={
            "data": {
                "values": person_data
            },
            "matching_attribute": "email_addresses"
        }
    )
    return resp.json()def assert_company(company_data):
    """Create or find a Company record in Attio (upsert by domain)."""
    resp = requests.put(
        f"{ATTIO_API_URL}/objects/companies/records",
        headers={
            "Authorization": f"Bearer {ATTIO_ACCESS_TOKEN}",
            "Content-Type": "application/json"
        },
        json={
            "data": {
                "values": company_data
            },
            "matching_attribute": "domains"
        }
    )
    return resp.json()@app.route("/browse-ai-webhook", methods=["POST"])
def handle_webhook():
    payload = request.get_json()    if payload.get("event") != "taskFinishedSuccessfully":
        return jsonify({"status": "ignored"}), 200    task = payload["task"]
    captured = task.get("capturedTexts", {})    results = {}    # Create or find the company first (if we have domain/name)
    website = captured.get("website", "")
    company_name = captured.get("company_name", "")
    if website or company_name:
        company_data = {}
        if company_name:
            company_data["name"] = [{"value": company_name}]
        if website:
            # Extract domain from URL
            domain = website.replace("https://", "").replace("http://", "").split("/")[0]
            company_data["domains"] = [{"domain": domain}]
        results["company"] = assert_company(company_data)    # Create or find the person
    email = captured.get("email", "")
    if email:
        person_data = {
            "email_addresses": [{"email_address": email}]
        }
        first_name = captured.get("first_name", "")
        last_name = captured.get("last_name", "")
        if first_name or last_name:
            person_data["name"] = [{"first_name": first_name, "last_name": last_name}]
        if captured.get("phone"):
            person_data["phone_numbers"] = [{"phone_number": captured["phone"]}]
        if captured.get("job_title"):
            person_data["job_title"] = [{"value": captured["job_title"]}]        results["person"] = assert_person(person_data)    return jsonify({"status": "success", "results": results}), 200if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Step 3: Register the webhook in Browse AI

Via the dashboard:

  1. Open your robot and go to the Integrate tab.

  2. Under Webhooks, click Add webhook.

  3. Paste your endpoint URL (e.g. https://yourdomain.com/browse-ai-webhook).

  4. Select the taskFinishedSuccessfully event.

Via the API:

curl -X POST "https://api.browse.ai/v2/robots/YOUR_ROBOT_ID/webhooks" \
  -H "Authorization: Bearer YOUR_BROWSE_AI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourdomain.com/browse-ai-webhook",
    "events": ["taskFinishedSuccessfully"]
  }'

πŸ’‘ IP allowlisting: Browse AI sends webhooks from IP address 3.228.254.190. If your server has a firewall, add this to your allowlist. See Webhooks: IP address for allowlisting.

Method 3: API polling (batch/scheduled)

If you prefer to pull data on a schedule rather than receive it in real time, you can poll the Browse AI API for completed tasks and push results into Attio in batches.

Example: Poll and sync to Attio

import requests
from datetime import datetime, timedeltaBROWSE_AI_API_KEY = "your_browse_ai_api_key"
ROBOT_ID = "your_robot_id"
ATTIO_ACCESS_TOKEN = "your_attio_access_token"def get_recent_tasks(since_hours=1):
    """Fetch tasks completed in the last N hours."""
    resp = requests.get(
        f"https://api.browse.ai/v2/robots/{ROBOT_ID}/tasks",
        headers={"Authorization": f"Bearer {BROWSE_AI_API_KEY}"},
        params={"pageSize": 100}
    )
    tasks = resp.json().get("result", {}).get("robotTasks", {}).get("items", [])    cutoff = datetime.utcnow() - timedelta(hours=since_hours)
    return [t for t in tasks if t.get("status") == "successful"
            and datetime.fromisoformat(t["finishedAt"].replace("Z","")) > cutoff]def sync_to_attio(tasks):
    """Assert Person records in Attio from Browse AI task results."""
    for task in tasks:
        captured = task.get("capturedTexts", {})
        email = captured.get("email", "")
        if not email:
            continue        person_data = {
            "email_addresses": [{"email_address": email}],
            "name": [{"first_name": captured.get("first_name", ""),
                      "last_name": captured.get("last_name", "")}]
        }        requests.put(
            "https://api.attio.com/v2/objects/people/records",
            headers={
                "Authorization": f"Bearer {ATTIO_ACCESS_TOKEN}",
                "Content-Type": "application/json"
            },
            json={
                "data": {"values": person_data},
                "matching_attribute": "email_addresses"
            }
        )# Run this script on a schedule (e.g. cron job every hour)

πŸ“– For full Browse AI API details, including pagination, bulk operations, and task filtering, see the API Guide: Getting started and API Guide: Bulk operations.

Attio-specific tips

Using the assert endpoint (built-in deduplication)

Attio's PUT /objects/{object}/records endpoint with a matching_attribute parameter is the recommended way to create records. It works as an upsert:

  • People: Match on email_addresses. If a person with that email exists, it returns the existing record.

  • Companies: Match on domains. If a company with that domain exists, it returns the existing record.

This means you don't need to search before creating. The assert endpoint handles deduplication for you in a single API call.

Working with different objects

Attio uses the same API pattern for all objects. Change the object slug in the URL:

  • People: /v2/objects/people/records

  • Companies: /v2/objects/companies/records

  • Deals: /v2/objects/deals/records

  • Custom objects: /v2/objects/{your_custom_object_slug}/records

Rate limits

Attio allows 100 read requests per second and 25 write requests per second. If you're syncing large batches, add a small delay between write operations to stay within limits.

Common field mappings

Here are typical mappings between Browse AI captured fields and Attio Person attributes:

Browse AI field

Attio attribute

Notes

email

email_addresses

Used for deduplication

first_name + last_name

name

Object with first_name and last_name keys

phone

phone_numbers

job_title

job_title

company_name

Company name

Create a separate Company record and link

website

Company domains

Used for Company deduplication

(origin URL)

Custom attribute or note

Available as task.inputParameters.originUrl

πŸ’‘ Linking People to Companies: In Attio, People and Companies are separate objects. Once you've created both records, Attio automatically links them if the person's email domain matches the company's domain. No manual association needed.

Sending monitoring data to Attio

If you're using Browse AI monitors to track changes on websites, you can push change events into Attio. Use the taskCapturedDataChanged webhook event to trigger updates. For example, you could update a Person's job title when their LinkedIn profile changes, or add a note to a Company record when their pricing page is updated.

Troubleshooting

Attio returns a 401 Unauthorized error

Check that your access token is correct and that the integration hasn't been deleted. Go to Workspace Settings β†’ Developers in Attio to verify.

Attio returns a 422 validation error

The attribute values are in the wrong format. Attio requires values to be wrapped in arrays with specific key names. For example, email must be [{"email_address": "..."}], not just a plain string.

Attio returns a 429 Too Many Requests error

You've hit the write rate limit (25 requests per second). Add a small delay between requests or batch your operations with brief pauses.

Records aren't linking to companies

Attio links People to Companies by matching email domain to company domain. Make sure you've created the Company record with the correct domain before creating the Person, or create both and let Attio auto-link.

Webhook isn't firing

Make sure the webhook URL is publicly accessible and that your server responds with a 200 status code. See the Webhooks: Set up guide for detailed debugging steps.

Did this answer your question?