# Bettercontact Enrichment

> Find and qualify work-email candidates with BetterContact's asynchronous API while controlling credits, preserving row identity, and separating deliverable results from catch-all or uncertain records. Use when a user asks to check a BetterContact balance, test the service, enrich a confirmed person-and-company list, run a bounded benchmark, or prepare evidence-safe candidates for later review. Requires explicit approval before any credit-consuming request and never sends outreach or writes to a CRM.

- Skill: `seryozh/bettercontact-enrichment` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add seryozh/bettercontact-enrichment`
- Raw SKILL.md: https://api.skillmd.com/api/skills/seryozh/bettercontact-enrichment/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Seryozh (https://skillmd.com/u/seryozh)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/seryozh/bettercontact-enrichment

---


# BetterContact Enrichment

Use BetterContact as a contact-data finder, not as proof of a person's identity, current role, or permission to contact them. Finish with a traceable candidate table and credit receipt. Do not turn a returned address into outreach or a CRM mutation.

Read [references/api-contract.md](references/api-contract.md) before making an API call. Use [scripts/bettercontact_enrich.py](scripts/bettercontact_enrich.py) for repeatable previews and bounded runs.

## Set the boundary

Classify the requested action before proceeding:

- `preview`: validate a local CSV and estimate the maximum contacts exposed and credits at risk; no network call;
- `account`: read the current credit balance; no credits consumed;
- `run`: submit personal and company data to BetterContact and potentially consume credits;
- `resume`: retrieve an already submitted request from its saved receipt without another POST;
- `handoff`: return candidates for human review or a separate, authorized workflow; never send or write them automatically.

A `run` is an external, credit-consuming action. Show the exact row count, the input file, the fields being shared, and the maximum one-credit-per-row planning exposure. Obtain explicit user approval for that run. Earlier approval for a different file, row count, or enrichment type does not carry over.

## Require confirmed identity inputs

Enrich only after the person, current company, and company domain have been established from a user-supplied authoritative record or first-party evidence. Never ask BetterContact to resolve an ambiguous identity and then treat its own answer as confirmation.

Require these CSV columns:

- `source_id`: stable unique identifier, not a row number that will change after sorting;
- `first_name` and `last_name`;
- `company`;
- `company_domain`.

Accept `linkedin_url` only when already known. Leave unsupported values empty instead of inferring them. Deduplicate by `source_id` and identity before submission. Preserve the source evidence outside the BetterContact receipt.

## Use the existing `.env`

Use the current project's already authorized `.env` file. Resolve its path from the workspace or global instructions, load `BETTERCONTACT_API_KEY` into the process without printing the value, and continue. Do not invent a connector, duplicate key store, or extra secret-management layer. The helper deliberately has no `--api-key` option.

## Preview before spending credits

Run:

```bash
python3 <skill-dir>/scripts/bettercontact_enrich.py preview \
  --input <confirmed-contacts.csv> \
  --max-rows 10
```

Inspect the returned count, input hash, source IDs, and fields to be shared. Resolve validation errors before asking for approval. Start a new integration or changed input shape with one positive-control contact whose work address is already known. Expand only after the returned identity, status, custom identifier, and credit ledger behave as expected.

Use one lead per request by default. This makes row mapping and partial-failure recovery explicit. Do not switch to large batches merely because the API permits up to 100 leads.

## Check balance and run

Load the existing `.env` so `BETTERCONTACT_API_KEY` is available to the process, then check the balance:

```bash
python3 <skill-dir>/scripts/bettercontact_enrich.py account
```

After the user approves the exact preview, run email-only enrichment:

```bash
python3 <skill-dir>/scripts/bettercontact_enrich.py run \
  --input <confirmed-contacts.csv> \
  --output <private-receipt.json> \
  --max-rows 10 \
  --approved-sha256 <exact-hash-from-preview> \
  --execute
```

The helper binds execution to the exact preview SHA-256 and sends the in-memory snapshot represented by that hash. It permits only one live `run` or `resume` process at a time, always requests email enrichment, disables phone enrichment, omits webhooks, submits one lead at a time, rate-limits calls, refuses redirects, does not retry a `POST`, polls to a bounded timeout, and atomically checkpoints every credit-risk transition: before submission, after receiving a request ID, and after a terminal result or failure. Do not bypass those defaults without a new, evidence-backed reason and explicit approval for the changed data or cost exposure.

If a submitted request times out or retrieval fails, do not submit the contact again. Resume from the private receipt so the saved request ID and source identity are checked and the same receipt is updated atomically:

```bash
python3 <skill-dir>/scripts/bettercontact_enrich.py resume \
  --receipt <private-receipt.json>
```

Use `--source-id` only when the receipt contains more than one resumable request. The helper prints only a compact status summary; the returned name, address, and title remain inside the private receipt. Reconcile that result before considering any new request.

## Classify conservatively

Use the helper's disposition and preserve the raw BetterContact status:

| Disposition | Meaning | Action |
|---|---|---|
| `DELIVERABLE_CANDIDATE` | Exact `deliverable`, non-generic mailbox, submitted name matches, and email domain matches the confirmed company domain | Candidate for separate preflight and human review |
| `QUARANTINE_CATCH_ALL` | Any catch-all status, including `catch_all_safe` | Do not treat as safely deliverable |
| `QUARANTINE_GENERIC` | Role mailbox rather than a person-specific address | Hold unless the user explicitly wants a generic channel |
| `QUARANTINE_IDENTITY` | Returned name is missing or differs from the submitted identity | Resolve identity from authoritative evidence |
| `QUARANTINE_DOMAIN` | Email domain differs from the confirmed company domain | Require first-party evidence of the domain relationship |
| `REJECT_UNDELIVERABLE` | Explicitly undeliverable | Reject |
| `NOT_FOUND` | No address returned and the response reports not found | Leave empty |
| `ERROR_*` | Malformed, ambiguous, timed-out, or failed result | Investigate; never silently promote |

Do not infer provenance from `email_provider`; it may describe mailbox infrastructure rather than the upstream finder. Do not overwrite an authoritative job title with `contact_job_title`. A successful API response is not evidence that outreach is appropriate.

## Reconcile the receipt

Before declaring the run complete, verify:

1. every submitted `source_id` has one request ID or an explicit pre-submission error;
2. every completed request has a terminal response and one disposition;
3. the sum of per-request `credits_consumed` is recorded;
4. the before-and-after account balance is recorded when the API returned both;
5. any difference between the reported sum and balance delta is flagged, not explained away;
6. the receipt contains no API key;
7. the output table separates candidates, quarantines, rejects, not-found rows, and errors.

If a checkpoint says `SUBMISSION_AMBIGUOUS`, assume the job may exist even without a request ID. Check the saved balance and the BetterContact request history before any resubmission.

Trust the live ledger over marketing language. A catch-all label does not guarantee a free request. Keep the receipt private because it contains personal data.

## Report and stop

Show the user a compact table with `source_id`, person, company, returned address, raw status, disposition, and credits consumed. Then report counts, credits before and after, receipt path, input hash, and remaining uncertainty.

State explicitly that no outreach was sent and no CRM was changed. Stop at the candidate handoff unless the user separately authorizes the exact next action.

