Hand the agent a CSV or XLSX with LinkedIn profile links — it enriches every row with live profile data (headline, role, company, location, skills) and saves a clean, styled XLSX back. Built for CRM exports, event lists, and sourcing sheets.
/api/v1/profile/full10 credits/callBallpark: ~100 credits for 10 typical runs. Signup includes 300 free credits.
Download comes with your API key baked in — sign up (300 free credits, one minute) or open this skill in your dashboard.
---
name: linkedin-enrich-and-save
description: Use when the user hands over a spreadsheet (CSV or XLSX) containing LinkedIn profile URLs and wants each row enriched with live profile data (headline, role, company, location, followers, skills) and saved as a new, nicely formatted XLSX. Triggers on "enrich this list", "fill in the details for these profiles", "clean up my leads sheet", "add LinkedIn data to this export".
---
# Enrich & Save (via Zooq)
You take a spreadsheet with LinkedIn profile links, enrich every row with live profile data through the Zooq API, and write a new, styled XLSX the user can open and share. You need code execution for this skill (pandas + openpyxl, or openpyxl alone).
## Inputs you need from the user
- **The file**: a `.csv` or `.xlsx` containing LinkedIn profile URLs somewhere in it. You auto-detect the column — don't make the user name it unless detection fails.
- Nothing else. Keep every original column; you only append.
## Step 0 — read the file and find the profiles
```python
import pandas as pd, re
df = pd.read_excel(PATH) if PATH.endswith(".xlsx") else pd.read_csv(PATH)
HANDLE_RE = re.compile(r"linkedin\.com/in/([^/?#\s]+)", re.I)
def extract_handle(cell):
m = HANDLE_RE.search(str(cell))
if not m:
return None
# strip trailing slashes and url-encoding leftovers
from urllib.parse import unquote
return unquote(m.group(1)).strip("/")
# pick the column with the most linkedin.com/in/ hits
best = max(df.columns, key=lambda c: df[c].astype(str).str.contains("linkedin.com/in/", case=False).sum())
df["_handle"] = df[best].map(extract_handle)
```
Report what you found before spending anything: "N rows, M with a LinkedIn profile URL (column '<name>'), K unique handles."
## Step 1 — the cost gate (do NOT skip)
Every enrichment is one call to `/api/v1/profile/full` at **10 credits per row**. Tell the user the exact total (unique handles × 10) and, if the list is over 50 rows, get an explicit yes before proceeding. Deduplicate first — the same handle twice should cost once (enrich once, fill both rows).
## Step 2 — enrich, one call per profile
Call profiles ONE AT A TIME. Do not attempt `/api/v1/profile/enrich-bulk` — that batch endpoint currently rejects submissions upstream; the per-profile GET below is the reliable path.
```
GET https://zooq.dev/api/v1/profile/full?handle=<HANDLE>
Headers:
X-API-Key: REPLACE_WITH_YOUR_KEY
```
```python
import requests, time
def enrich(handle):
r = requests.get(
"https://zooq.dev/api/v1/profile/full",
params={"handle": handle},
headers={"X-API-Key": "REPLACE_WITH_YOUR_KEY"},
timeout=60,
)
body = r.json()
if r.status_code == 200 and body.get("success"):
return "enriched", body["data"], r.headers.get("X-Zooq-Credits-Remaining")
if r.status_code == 404:
return "not found", None, r.headers.get("X-Zooq-Credits-Remaining") # billed — a real lookup ran
return f"error {r.status_code}", None, r.headers.get("X-Zooq-Credits-Remaining") # 4xx validation errors are not billed
results = {}
for h in df["_handle"].dropna().unique():
results[h] = enrich(h)
time.sleep(0.6) # default limit is 120 req/min — stay comfortably under it
```
Fields worth pulling from `data`: `first_name` + `last_name`, `headline`, current position from `full_positions[]` (the entry with `is_current: true` — its `title` and company name), `geo_city` + `geo_country_code`, `follower_count`, `connection_count`, top 5 `skills[].name`, most recent `education[]` school.
## Step 3 — write the styled XLSX
Append these columns to the original data, then save `<original-name>-enriched.xlsx` next to the input file: Full name · Headline · Current title · Company · Location · Followers · Connections · Top skills · Education · Status.
```python
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
out = PATH.rsplit(".", 1)[0] + "-enriched.xlsx"
df.drop(columns=["_handle"]).to_excel(out, index=False, sheet_name="Enriched")
from openpyxl import load_workbook
wb = load_workbook(out); ws = wb.active
header_fill = PatternFill("solid", fgColor="1B543E")
for cell in ws[1]:
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = header_fill
cell.alignment = Alignment(vertical="center")
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
for col in ws.columns:
width = max(len(str(c.value or "")) for c in col[:50]) + 2
ws.column_dimensions[get_column_letter(col[0].column)].width = min(width, 48)
wb.save(out)
```
## Step 4 — report
One short summary, not a wall of text: rows processed, enriched / not found / skipped counts, credits spent (start balance minus the last `X-Zooq-Credits-Remaining` you saw), and the output filename. If more than a handful of rows came back "not found", say so plainly — dead URLs in the source sheet are the usual cause, not an API problem.
## Failure rules
- A row with no LinkedIn URL is "skipped", never an error. Keep it in the output.
- Never fabricate profile data for failed rows — leave the enrichment columns empty and set Status.
- On `429` (rate limit), wait 30 seconds and continue where you left off.
- On `402` (out of credits), stop immediately, save what you have, and tell the user how many rows remain and that they can top up at https://zooq.dev/billing.