Datacircle

LinkedIn profiles in Airtable: enrich records with a Run a script action

Your Airtable table has a LinkedIn profile URL in each record, and you want the person's job title, company and location in the same record. Airtable's LinkedIn integration can't: it posts records as status updates on LinkedIn, through Zapier.

Datacircle is a data co-op. Step 1: Query your favorite B2B data APIs through us. Same request, same price, no markup. Step 2: You're DONE. Every morning, you get the flat file of your data plus everyone else's. Right now we have 3 live LinkedIn profile APIs that we trust: Up2Data, HarvestAPI and Fetchin.

One automation fills each new record. Its Run a script action gets the profile back, at $2.375 per 1,000 profiles Up2Data finds, and nothing for a profile it can't find. Past Up2Data's daily limit, the same script calls Fetchin, at $1.485 per 1,000. It writes a status for every answer, so a private or deleted profile can't stop the automation. You send the provider's own request to api.datacircle.dev, with your Datacircle key. That's the only change.

We ran the script unchanged in Node.js, with the input, base and fetch an automation script gets: against a mock of our API for every answer below, then against api.datacircle.dev with a wrong key. We have no Airtable account, so we haven't run it in a base.

Before you start

  • An Airtable plan with the Run a script action: Team or above. Airtable's Free plan and a Team plan trial don't have it.
  • A Datacircle API key: Log in at datacircle.dev/login with your work email. Your API key is on the page once you're in. You get a $5 credit, enough for 2,105 profiles through Up2Data.
  • A table with a LinkedIn URL field, holding URLs like https://www.linkedin.com/in/williamhgates.

The fields the script writes

Add these fields to your table. The script writes each one, whatever our API answers:

The fields the script writes and their type
FieldTypeWhat it gets
StatusSingle line textthe API's answer, as the table below shows
Job titleSingle line textthe person's current job title
CompanySingle line textthe company of that job
LocationSingle line textthe location on the profile
HeadlineSingle line textthe line under the person's name on LinkedIn
Cost (USD)Number, with 6 decimal placesthe charge our API reports for the call, 0 if Airtable timed out

Other field names work too: change them on the script's FIELDS line. Our LinkedIn Profile API page shows the profile's other fields, such as jobs and schools.

The trigger

Create an automation, and pick When record matches conditions as its trigger:

The trigger's settings
SettingValue
Tableyour table
Conditionsyour LinkedIn URL field is not empty, and Status is empty

The automation runs on a new record as soon as you fill in its LinkedIn URL. The script then writes its Status, so the record no longer matches and the automation doesn't run on it again. To run a record again, clear its Status.

The Run a script action

Add a Run a script action, and give it two input variables:

The script's input variables
NameValue
recordIdthe trigger record's Airtable record ID
linkedinUrlthe trigger record's LinkedIn URL field

Then paste the code below. Put your key between the quotes on the KEY line and your table's name on the TABLE line.

// LinkedIn profiles in Airtable, through Datacircle: Up2Data, then Fetchin past Up2Data's daily limit.
// Input variables: recordId, the trigger's record ID, and linkedinUrl, its LinkedIn URL. Your key goes in KEY.
const KEY = "your Datacircle API key"
const TABLE = "People" // your table's name
// Your fields' names: single line text, and a number field for the cost
const FIELDS = { status: "Status", title: "Job title", company: "Company", location: "Location", headline: "Headline", cost: "Cost (USD)" }

const { recordId, linkedinUrl } = input.config()

async function call(method, path, provider, body) {
  const headers = { Authorization: "Token " + KEY, "X-Data-Provider": provider }
  if (body) headers["Content-Type"] = "application/json"
  let response
  try {
    response = await fetch("https://api.datacircle.dev" + path, { method, headers, body: body && JSON.stringify(body) })
  } catch (error) {
    return { status: "no answer", body: { error: error.message } } // Airtable stops a fetch after 30 seconds
  }
  const answer = await response.json().catch(() => ({})) // a gateway's error page isn't JSON: its status says enough
  return { status: response.status, body: answer }
}

function row(status, found = {}) {
  const all = { status, title: "", company: "", location: "", headline: "", cost: 0, ...found }
  for (const key in all) if (all[key] === undefined || all[key] === null) all[key] = key === "cost" ? 0 : ""
  return all
}

function failed(answer) {
  const reason = (answer.body.error && answer.body.error.message) || answer.body.error
  return row(reason ? "error " + answer.status + ": " + reason : "error " + answer.status)
}

async function profile(url) {
  const up2data = await call("POST", "/v1/profiles/enrich", "up2data", { url })
  if (up2data.status === 200) {
    const data = up2data.body.data
    const company = data.current_company || {}
    return row("found", {
      title: company.title, company: company.name, location: (data.location || {}).raw, headline: data.headline,
      cost: up2data.body.datacircle_meta.cost_usd,
    })
  }
  if (up2data.status === 422) return row("not found: private or deleted, free")
  if (up2data.status === 400) return row("not a profile URL, free")
  if (up2data.status !== 429) return failed(up2data)

  // Up2Data's daily limit: the same URL through Fetchin, with the same key
  const fetchin = await call("GET", "/api/v1/profile?profileUrlOrUrn=" + encodeURIComponent(url), "fetchin")
  const body = fetchin.body
  if (fetchin.status === 200) {
    return row("found (Fetchin)", {
      title: body.jobTitle, company: body.companyName, location: body.location, headline: body.title,
      cost: body.datacircle_meta.cost_usd,
    })
  }
  if (fetchin.status === 404 && body.code === "PROFILE_NOT_FOUND") {
    return row("not found (Fetchin): private or deleted, billed", { cost: body.datacircle_meta.cost_usd })
  }
  if (fetchin.status === 400) return row("not a profile URL, free")
  return failed(fetchin)
}

const found = await profile(linkedinUrl)
const update = {}
for (const key in FIELDS) update[FIELDS[key]] = found[key]
await base.getTable(TABLE).updateRecordAsync(recordId, update)
output.set("status", found.status)

Airtable's test button makes a real call, and we bill that call like any other.

Anyone who can edit the automation sees your key in the script. If your Datacircle key leaks, send a POST to https://api.datacircle.dev/me/api-key/reset/ with it, as our quickstart shows: you get a new key, and the old one stops working.

Each API response: its cost and the Status the script writes

Up2Data's and Fetchin's responses
AnswerWhat it meansCostStatus
Up2Data 200the profile$2.375 per 1,000found
Up2Data 422Up2Data can't reach that profile: private or deletedfreenot found: private or deleted, free
Up2Data 400not a LinkedIn profile URL: a company page or a malformed URLfreenot a profile URL, free
Up2Data 429Up2Data's daily limit, or its rate limitfreenone: the script sends the same URL to Fetchin
Fetchin 200the profile$1.485 per 1,000found (Fetchin)
Fetchin 404 with PROFILE_NOT_FOUNDFetchin can't find that profile: private or deleted$1.485 per 1,000: Fetchin bills the lookupnot found (Fetchin): private or deleted, billed
Fetchin 400not a LinkedIn profile URL. which Fetchin sees only past Up2Data's daily limit: there our API answers 429 without sending the URL to Up2Datafreenot a profile URL, free
any other401: your key is wrong. 402: your balance is too low. 502, 503 or 504: Up2Data or Fetchin failed. 429: Fetchin's rate limit, which all our customers sharefreeerror, the status and the reason
no answerAirtable stops a request after 30 seconds, and our API waits up to 45 seconds for a slow providerwe bill it only if the provider answered after Airtable stoppederror no answer

The script writes a Status for every answer, so a private profile can't stop the automation. For an error record, fix what its reason says, then clear its Status to run it again. A record with error no answer reads 0 in Cost (USD) even when we billed the call: your balance shows the charge. If you run it again, we bill the new call like any other. Airtable counts every run against your plan, failed or not.

Up2Data takes $1 a day per account (421 profiles), with a shared daily limit for all customers, then answers 429 until 00:00 UTC. HarvestAPI has no daily limit. Fetchin has no daily limit either. A call your balance can't cover answers 402. Add funds, from $5, on your dashboard.

The records already in your table

Airtable runs the automation on a record when it starts to match the conditions. It skips records that already match when you turn it on. To fill the records you have:

  1. Before you turn the automation on, type any word, like wait, in their Status.
  2. Turn the automation on.
  3. Clear their Status. Each record starts to match, and the automation runs on it.

Each record is one run. Airtable allows 25,000 runs a month per workspace on Team and 100,000 on Business, so a table with more records than that takes more than a month. Up2Data fills at most 421 records a day, and Fetchin fills the rest. If many records run at once, some may hit Fetchin's rate limit, which all our customers share: they get error 429, and you clear their Status to run them again.

Cost per 1,000 profiles

We charge your balance these prices, with no markup, as our pricing page shows:

Our price per 1,000 profiles and daily limit, by provider
Up2DataFetchin
Per 1,000 found$2.375$1.485
Per 1,000 not foundfree$1.485
Daily limit421 profiles per accountnone

If 1,000 new records come in one day and the providers find every profile, you pay us $1.86 at most: $1.00 for 421 through Up2Data and $0.86 for the other 579 through Fetchin. Fetchin costs less per profile found but bills a profile it can't find, which Up2Data doesn't. Trying Up2Data first costs less only when more than about 37% of your URLs are private or deleted. The script tries it first because you may not know how many of your URLs are gone, and a profile Up2Data can't find costs nothing. If the providers find more than about 63% of yours, Fetchin alone costs less: delete the script's lines from const up2data to return failed(up2data), and the script sends each record to Fetchin.

The same call in n8n: LinkedIn profiles in n8n. In a Zap: LinkedIn profiles in Zapier. In a Make scenario: LinkedIn profiles in Make. In a Clay table: LinkedIn profiles in Clay. In a Google Sheet: LinkedIn profiles in Google Sheets. Other vendors' prices per 1,000: LinkedIn profile API pricing compared.

Questions

Can Airtable get LinkedIn profile data?

Yes: add an automation with a Run a script action that calls a LinkedIn profile API. The script sends the profile's URL and writes the fields our API returns into the record. With our API: POST {"url": "<the profile's LinkedIn URL>"} to https://api.datacircle.dev/v1/profiles/enrich, with the headers Authorization: Token <your key> and X-Data-Provider: up2data. Once you pass Up2Data's daily limit, the same script sends the URL to Fetchin: GET https://api.datacircle.dev/api/v1/profile?profileUrlOrUrn=<the profile's LinkedIn URL>, with X-Data-Provider: fetchin.

Does it work on Airtable's Free plan?

No. Airtable's Free plan and a Team plan trial don't have the Run a script action. Airtable counts one automation run per record: Team has 25,000 runs a month per workspace, Business 100,000. We haven't run it on any Airtable plan.

Why doesn't the automation fill the records already in my table?

Airtable runs an automation on a record when the record starts to match the trigger's conditions. It skips records that already match. Before you turn the automation on, type any word in the Status of the records you have. Turn it on, then clear their Status: each record starts to match, and the automation runs on it.

Can't Airtable's AI fill in a job title?

Airtable's field agents can search the web for a person's job title, on paid plans. Airtable says this often takes extra AI credits and that you should check the results. We get the live profile from the person's LinkedIn URL, and the script writes the job title on the profile.

How much does it cost to enrich LinkedIn profiles in Airtable?

$2.375 per 1,000 through Up2Data (a profile it can't find is free), $3.70 per 1,000 through HarvestAPI. $1.485 per 1,000 through Fetchin, a profile it can't find billed the same. HarvestAPI bills a profile it can't find at $2.30 per 1,000. Airtable counts each automation run against your plan, and we take each call's price from your Datacircle balance. Say the providers find 1,000 profiles in a day: you pay at most $1.86, $1.00 for 421 through Up2Data and $0.86 for 579 through Fetchin.

Is there a daily limit?

Up2Data takes $1 a day per account (421 profiles), with a shared daily limit for all customers, then answers 429 until 00:00 UTC. HarvestAPI has no daily limit. Fetchin has no daily limit either.

Is each request live, or cached?

Live. Each request goes to the provider and gets the profile as it is today.

Do I need a LinkedIn account?

No. You send the profile's URL with your Datacircle key: no LinkedIn login, no cookies, no browser.

What happens when my balance runs out?

A call your balance can't cover answers 402. Add funds, from $5, on your dashboard.

Get started

Sign up at datacircle.dev with your work email: a $5 credit, that's 2,105 LinkedIn profiles at $2.375 per 1,000.

Sign up
Ask AI about Datacircle

Each opens with our question