---
name: everflow-enrich
description: Enrich a CSV (or a few records) of people or companies with Everflow's enrichment API — person profile (title, company, location), work email + mobile phone, or company firmographics (industry, headcount, revenue, funding, HQ), and find alternate contacts (someone in a similar role at the same company when a contact isn't found, has no email, or left). Use when the user hands over a contact/company list, CSV, or spreadsheet and asks to enrich, fill in, append data, find emails/phones, find a replacement/alternate/similar contact at a company, or "finish it out".
---

# Everflow enrichment

Everflow's enrichment API runs at `https://sales-ext.everflow-resource-hub.com`
(Everflow's internal tools domain, the same service that backs the EF Sales
Chrome extension). It wraps Clay behind our own
capabilities. **Everything is plain HTTPS. Use `curl`.** Nothing needs
installing, and the server does the CSV parsing, dedupe, and merging.

- **A file (CSV) → CSV jobs:** upload the file, wait, then download the enriched file.
- **A few records mentioned in chat (≤ ~10) → JSON:** one call with `"wait": 60`, then show the results inline.
- **Contact not found / no email → alternate contacts:** find someone in a similar role at the same company.

Never call Clay directly and never ask for a Clay key.

## API key

Every call needs the header `x-api-key: efk_...`. Look for the key in this order:
1. `$EVERFLOW_API_KEY`
2. `~/.claude/skills/everflow-enrich/.env` (the line `EVERFLOW_API_KEY=efk_...`)
3. Otherwise ask the user. They create a key at
   https://sales-ext.everflow-resource-hub.com/api-keys by signing in with their
   @everflow.io Google account. If you can write files, save it to that `.env`
   (`chmod 600`) so they aren't asked again.

```bash
KEY="${EVERFLOW_API_KEY:-$(sed -n 's/^EVERFLOW_API_KEY=//p' ~/.claude/skills/everflow-enrich/.env 2>/dev/null)}"
API=https://sales-ext.everflow-resource-hub.com
```

Never print the key in chat. A `401` means the key is wrong or revoked: ask for a new one.

## Capabilities

| capability | input (need one) | adds | credits/row |
|---|---|---|---|
| `person` | `linkedin_url` or `email` | name, title, headline, company, company_domain, company LinkedIn, location, country | ~1 |
| `person_contact` | `linkedin_url` | **work_email, mobile_phone** + person fields | **~18** |
| `company` | `company` (domain, website, or company LinkedIn URL) | company_name, company_domain, industry, subindustry, size, employee_count, annual_revenue, total_funding, founded, HQ, company_description | ~1 |

Plus **alternate contacts** (`POST /v1/alt-contacts`): someone in a similar
role at the same company. Input: `company` (+ optional `title`). No credits,
~30–60s. See "Alternate contacts" below.

`GET $API/v1/capabilities` returns the live list of enrichments (inputs, outputs, cost).

Picking one:
- "enrich contacts" / "fill in titles": use `person`.
- "find emails / phones": use `person_contact` (about 18× the cost, so always confirm first).
- "enrich companies / firmographics": use `company`.
- "enrich everything" on a contact list: run `person`, then run `company` on
  the downloaded file (it automatically uses `ef_company_domain` if there's no
  better company column).

## CSV workflow

**1. Dry run.** This spends nothing. It shows the detected column mapping, row counts and cost:
```bash
curl -s -X POST "$API/v1/enrich/person/csv?dry_run=1" \
  -H "x-api-key: $KEY" -H "Content-Type: text/csv" --data-binary @contacts.csv
```
```json
{ "capability": "person", "mapping": { "linkedin_url": "LinkedIn URL" },
  "columns": ["..."], "rows": 250, "unique_lookups": 231, "already_done": 0,
  "missing_input": 19, "estimated_credits": 231, "dry_run": true }
```
Check that `mapping` points at the right column(s) against `columns`. To
override it, add `map_<input>=<Column>` query params, URL-encoded, e.g.
`&map_linkedin_url=Person%20LinkedIn`. For `person` you can map both
`map_linkedin_url` and `map_email`.

Tell the user the rows, unique lookups and estimated credits. If they asked
you to enrich and it's **≤ ~500 credits and not `person_contact`**, go ahead.
Otherwise wait for a yes.

**2. Start.** Send the same request without `dry_run` (keep any `map_` params):
```bash
curl -s -X POST "$API/v1/enrich/person/csv" \
  -H "x-api-key: $KEY" -H "Content-Type: text/csv" --data-binary @contacts.csv
```
→ `202 { "job_id": "ec_...", "status": "running", ... }`

**3. Wait.** Poll every ~5–10s until `status` isn't `running`:
```bash
curl -s "$API/v1/csv-jobs/ec_..." -H "x-api-key: $KEY"
```
- While running: `202 { "status": "running", "total": 231, "finished": 120 }`
- When done: `200 { "status": "complete", "lookups_ok": 220, "lookups_failed": 11, "download_url": "..." }`

Up to 100 unique lookups usually take under a minute. Larger files run as a
bulk job and can take several minutes, so tell the user it's working.

**4. Download** and save next to the input as `<name>.enriched.csv`:
```bash
curl -s "$API/v1/csv-jobs/ec_.../download" -H "x-api-key: $KEY" -o contacts.enriched.csv
```
The download keeps every original column and adds `ef_<field>` columns plus
`ef_<capability>_status` (`ok` / `failed` / `skipped: missing input`) and
`ef_<capability>_error`. Values from earlier passes are never blanked.
Re-uploading a file with `ef_<capability>_status = ok` rows skips those rows,
so they aren't charged twice.

**5. Report back:** the output path, how many rows were enriched / failed /
skipped, fill rates for the key fields, and anything odd (e.g. many
`skipped: missing input` usually means the wrong column was mapped).

If you can't write files (e.g. in the Claude app), show a summary table and
give the user the download command or offer the file as an attachment.

## Quick lookups (JSON)

```bash
curl -s -X POST "$API/v1/enrich/person" -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"wait":60,"items":[{"id":"1","linkedin_url":"https://www.linkedin.com/in/satyanadella"}]}'
```
Items are `{ "id": "<unique, ≤64 chars>", <input fields> }`, 1–100 per call.
Response: `{ "job_id": "ej_...", "status": "complete", "results": [ { "id": "1", "status": "ok", "data": { ... } } ], "skipped": [], "estimated_credits": 1 }`.
If it comes back `202` / `"running"`, poll `GET $API/v1/jobs/<job_id>`.
Add `"raw": true` for the full provider payload (large; only if needed).
Summarize `data` for the user. Don't dump raw JSON.

## Alternate contacts (similar role at the same company)

Use this when a person couldn't be enriched (`ef_person_status` = `failed`,
or no `ef_work_email`), they've left the company, or the user asks for "someone
else like them". It costs **no credits**, only search quota. Clay searches are
slow (about 30–60s each), so it runs as a job.

```bash
curl -s -X POST "$API/v1/alt-contacts" -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"company":"acme.com","title":"VP of Marketing","exclude_linkedin_url":"https://www.linkedin.com/in/original-person","limit":3,"wait":90}'
```
- `company`: a domain, website, or company LinkedIn URL (required).
- `title`: the original contact's title (optional; without it you get senior leaders).
- `exclude_linkedin_url`: the original person, so they aren't suggested back.
- `limit`: 1–10 candidates (default 3). `wait`: up to 90s.
- Batch: `{"items":[{"id":"row-7","company":"...","title":"..."}, ...], "limit":2}` (max 25).

It returns `200` when done within `wait`; otherwise `202 {"job_id":"ea_...","status":"running"}`. Poll
`GET $API/v1/alt-contacts/<job_id>` every ~10s.
```json
{ "job_id": "ea_...", "status": "complete", "matched_on": "similar_title",
  "search_term": "VP of Marketing / Marketing",
  "candidates": [ { "name": "Tony Cohn", "first_name": "Tony", "last_name": "Cohn",
      "title": "Senior Director, Customer Marketing", "company": "Everflow",
      "linkedin_url": "https://www.linkedin.com/in/...", "location": "...", "title_start_date": "2023-05" } ] }
```
In batch mode each lookup is in `results[]`, with its `id`.

`matched_on` tells the user how close the match is:
- `similar_title`: essentially the same role.
- `same_function`: same department, different level (e.g. VP → Director).
- `senior_leader`: nobody in that function, so Director+ at the company.
- `none`: nothing found.

Candidates come back ranked closest first. To get their email or phone, run
`person_contact` on their `linkedin_url` (~18 credits each, so confirm first).

For a CSV: after the person pass, collect the failed or email-less rows, then
batch them (≤25 per call) with the company column (or `ef_company_domain`),
the title column, and the row's LinkedIn URL as `exclude_linkedin_url`. Add
the top candidate as `ef_alt_name`, `ef_alt_title`, `ef_alt_linkedin_url`,
`ef_alt_match` columns. Offer to do this rather than doing it unasked.

## Errors

All errors are `{ "error": "message" }`. 400 errors on CSV jobs also include `columns`.

| status | meaning | what to do |
|---|---|---|
| 400 | bad input / no matching column | read `error` + `columns`; fix `map_` params |
| 401 | missing / invalid / revoked key | ask for a new key from `/api-keys` |
| 402 | Clay credits or quota exhausted | stop and tell the user (it's shared Everflow credit) |
| 404 | unknown capability or job | check `/v1/capabilities` or the job id |
| 409 | download before the job finished | keep polling |
| 429 | rate limited | wait a few seconds and retry |
| 502 | upstream provider error | retry once, then report |

## Google Sheets

For Sheets users there's an Apps Script with an "Everflow" menu:
https://sales-ext.everflow-resource-hub.com/skill/apps-script.gs. They paste it into
Extensions → Apps Script, save, reload, then use **Everflow → Set API key** and
**Everflow → Enrich …**.
