> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unifygtm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Import data from a CSV

> Upload data from a CSV file or spreadsheet into Unify.

## Overview

This tutorial walks through the process of creating Company records via API
using a simple script. We'll assume the starting point is a CSV of company data
to upload, but in general the approach here can be used for any data source.

The **Company** object is one of the standard objects provided by default in
Unify. Along with **Person**, it powers many of the core capabilities in Unify.
The unique identifier of a company is its website domain.

## Steps

<Steps titleSize="h3">
  <Step title="Prerequisites">
    Before you begin, be sure to [generate an API key](https://app.unifygtm.com/dashboard/settings/integrations/api-keys)
    if you don't already have one. You’ll pass it as an HTTP header on each request:

    ```http theme={null}
    X-Api-Key: <YOUR_API_KEY>
    ```

    In addition, you will need to install one of the official SDKs to follow along:

    <CodeGroup>
      ```bash Python theme={null}
      pip install unifygtm-sdk
      ```

      ```bash TypeScript theme={null}
      npm install @unifygtm/sdk csv-parse
      ```
    </CodeGroup>
  </Step>

  <Step title="Prepare a CSV of companies">
    Construct a CSV file that includes the companies you want to send into
    Unify. The columns don't need to match the attribute names in Unify since
    we'll map them over in the script.

    Here's an example CSV for the sake of this tutorial:

    ```csv example_companies.csv theme={null}
    Website,Name,LinkedIn,Score
    "tailscale.com","Tailscale","linkedin.com/company/tailscale",100
    "dagster.io","Dagster","linkedin.com/company/dagsterlabs",90
    "hashicorp.com","HashiCorp","linkedin.com/company/hashicorp",80
    "langchain.com","LangChain","linkedin.com/company/langchain",90
    "vercel.com","Vercel","linkedin.com/company/vercel",80
    "datadoghq.com","Datadog","linkedin.com/company/datadog",70
    "temporal.io","Temporal Technologies","linkedin.com/company/temporal-technologies",100
    "elastic.co","Elastic","linkedin.com/company/elastic-co",60
    "clickhouse.com","ClickHouse","linkedin.com/company/clickhouseinc",90
    "streamnative.io","StreamNative","linkedin.com/company/streamnative",70
    ```

    The only required column is a website or domain for the company. That's
    because Unify uses a company's domain as its unique identifier. Everything
    else is optional.
  </Step>

  <Step title="Understand the request payload">
    We’ll use the [upsert method](/developers/api/data/records/upsert) to create
    companies if they don't exist and update them if they do. The upsert method
    is the recommended way of sending data into Unify since it gives you the
    most control and never fails if a record already exists.

    The upsert requests we send will have two properties:

    * `match` -- This contains the value(s) used to match existing records. In
      this case, the only value we will use for matching is the `domain`.
    * `create_or_update_if_empty` -- This contains the values used when creating
      or updating the record. This will avoid overwriting existing values.

    <Tip>
      The `create_or_update_if_empty` property never overwrites values if the
      record already exists, which is a nice property. For full control over how
      existing records are updated, you can specify [more specific properties](/developers/api/data/records/upsert#overview)
      instead.
    </Tip>

    Here’s what a single request looks like for one company row:

    <CodeGroup>
      ```http Request theme={null}
      POST /data/v1/objects/company/records/upsert
      Host: api.unifygtm.com
      Content-Type: application/json
      X-Api-Key: <YOUR_API_KEY>

      {
        "match": {
          "domain": "unifygtm.com"
        },
        "create_or_update_if_empty": {
          "name": "Unify",
          "domain": "unifygtm.com",
          "description": "A nice company with a top-notch API.",
          "industry": "Software",
          "linkedin_url": "https://www.linkedin.com/company/unifygtm"
        }
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Assemble the script">
    The script needs to perform a few steps:

    1. Read the CSV
    2. Construct an upsert request payload for each row
    3. Send the requests

    Here's a complete script that does just that:

    <CodeGroup>
      ```python Python theme={null}
      import csv
      import sys
      from typing import TypedDict

      from unify import NotFoundError, Unify


      class Company(TypedDict):
          name: str | None
          domain: str
          linkedin_url: str | None
          account_score: float | None


      def read_csv(path: str) -> list[Company]:
          rows = []

          with open(path, newline="", encoding="utf-8") as f:
              reader = csv.DictReader(f)

              for i, raw in enumerate(reader, start=1):
                  domain = raw.get("Website")
                  if not domain:
                      print(f"[WARN] Row {i} skipped: missing required 'domain'")
                      continue

                  rows.append(
                      {
                          "name": raw.get("Name"),
                          "domain": domain,
                          "linkedin_url": raw.get("LinkedIn"),
                          "account_score": raw.get("Score"),
                      }
                  )

          return rows


      def main():
          if len(sys.argv) != 2:
              print("Usage: python upload_companies.py <path/to/companies.csv>")
              return 2

          # Read the CSV file
          companies = read_csv(path=sys.argv[1])
          if not companies:
              print("No valid rows to process.")
              return 0

          # Instantiate the client
          client = Unify()

          # Create a custom attribute for the account scores
          try:
              client.data.attributes.retrieve(
                  object_name="company",
                  attribute_name="account_score",
              )
          except NotFoundError:
              client.data.attributes.create(
                  object_name="company",
                  api_name="account_score",
                  display_name="Account Score",
                  description="Account score for the CSV upload guide.",
                  type="DECIMAL",
                  is_required=False,
                  is_unique=False,
              )
              print("Created custom attribute 'account_score' for companies.")

          # Create or update all records from the CSV file
          success = 0
          for row in companies:
              result = client.data.records.upsert(
                  object_name="company",
                  match={
                      "domain": row["domain"],
                  },
                  create_or_update_if_empty={
                      "domain": row["domain"],
                      "name": row["name"],
                      "linkedin_url": row["linkedin_url"],
                      "account_score": row["account_score"],
                  },
              )
              success += 1
              print(f"[OK] Upserted company {row['domain']} -> id={result.data.id}")

          print(f"Done. Upserted {success} of {len(companies)} companies.")


      if __name__ == "__main__":
          main()
      ```

      ```typescript TypeScript theme={null}
      import { readFileSync } from "fs";
      import { parse } from "csv-parse/sync";
      import Unify from "@unifygtm/sdk";
      import { UCompanyAttributes } from "@unifygtm/sdk/resources/data";

      interface CompanyRow {
        Name?: string;
        Website?: string;
        LinkedIn?: string;
        Score?: string;
      }

      function readCsv(path: string): UCompanyAttributes[] {
        const raw = readFileSync(path, "utf-8");
        const rows = parse(raw, { columns: true }) as CompanyRow[];

        const companies = new Array<UCompanyAttributes>();

        for (const row of rows) {
          if (!row.Website) {
            console.log(
              `[WARN] Skipping row with missing 'Website': ${JSON.stringify(row)}`,
            );
            continue;
          }

          companies.push({
            name: row.Name,
            domain: row.Website,
            linkedin_url: row.LinkedIn,
            score: row.Score ? parseFloat(row.Score) : undefined,
          });
        }

        return companies;
      }

      async function main(): Promise<void> {
        const path = process.argv[2];
        if (!path) {
          console.log("Usage: npx tsx upload_companies.ts <path/to/companies.csv>");
          process.exit(2);
        }

        const companies = readCsv(path);
        if (companies.length === 0) {
          console.log("No valid rows to process.");
          return;
        }

        const client = new Unify();

        // Create a custom attribute for the account scores
        try {
          await client.data.attributes.retrieve("account_score", {
            object_name: "company",
          });
        } catch (err: any) {
          if (err.status === 404) {
            await client.data.attributes.create("company", {
              api_name: "account_score",
              display_name: "Account Score",
              description: "Account score for the CSV upload guide.",
              type: "DECIMAL",
              is_required: false,
              is_unique: false,
            });
            console.log("Created custom attribute 'account_score' for companies.");
          } else {
            throw err;
          }
        }

        // Create or update all records from the CSV file
        let success = 0;
        for (const row of companies) {
          const result = await client.data.records.upsert("company", {
            match: {
              domain: row.domain,
            },
            create_or_update_if_empty: {
              name: row.name,
              domain: row.domain,
              linkedin_url: row.linkedin_url,
              account_score: row.score,
            },
          });
          success++;
          console.log(`[OK] Upserted company ${row.domain} -> id=${result.data.id}`);
        }

        console.log(`Done. Upserted ${success} of ${companies.length} companies.`);
      }

      main();
      ```
    </CodeGroup>
  </Step>

  <Step title="Run the script">
    Set your API key as an environment variable and then run the script:

    <CodeGroup>
      ```shell Python theme={null}
      export UNIFY_API_KEY="<YOUR_API_KEY>"
      python send_companies.py example_companies.csv
      ```

      ```shell TypeScript theme={null}
      export UNIFY_API_KEY="<YOUR_API_KEY>"
      npx tsx send_companies.ts example_companies.csv
      ```
    </CodeGroup>

    You should see output like this:

    ```text Output theme={null}
    [OK] Upserted company tailscale.com -> id=04caa07b-cbdd-4bb4-9de4-49abdc106aaa
    [OK] Upserted company dagster.io -> id=18cd4c36-3e6c-4778-88c3-4bd5c103f381
    ...
    Done. Upserted 10 of 10 companies.
    ```
  </Step>

  <Step title="Verify records exist (optional)">
    You can verify a specific company with the [Find unique record](/developers/api/data/records/find-unique)
    API. Simply send a request with a `match` property containing the domain of
    one of the companies the script ran on:

    <CodeGroup>
      ```http Request theme={null}
      POST /objects/company/find-unique

      {
        "domain": "tailscale.com"
      }
      ```

      ```json Response theme={null}
      {
        "status": "success",
        "data": {
          "object": "company",
          "id": "04caa07b-cbdd-4bb4-9de4-49abdc106aaa",
          "created_at": "2025-09-04T01:23:45Z",
          "updated_at": "2025-09-04T01:23:45Z",
          "attributes": {
            "name": "Tailscale",
            "domain": "tailscale.com",
            "linkedin_url": "linkedin.com/company/tailscale",
            "lead_source": "CSV Tutorial"
            // ...
          }
        }
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

## Conclusion

This approach works for any type of object in Unify, including both standard
objects and custom objects that you create. This script can be used as a
starting point and customized to send additional values, read from other data
sources, or send different types of records.
