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 before making an API call. Use 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_nameandlast_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:
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:
python3 <skill-dir>/scripts/bettercontact_enrich.py account
After the user approves the exact preview, run email-only enrichment:
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:
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:
- every submitted
source_idhas one request ID or an explicit pre-submission error; - every completed request has a terminal response and one disposition;
- the sum of per-request
credits_consumedis recorded; - the before-and-after account balance is recorded when the API returned both;
- any difference between the reported sum and balance delta is flagged, not explained away;
- the receipt contains no API key;
- 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.