Waterfalls
Ordered provider attempts that stop at the first usable result.
A waterfall tries providers in order until one returns a usable result.
Waterfalls find a value the row is missing. To check an address the row already has, see Email verification — a different chain with its own catch-all escalation.
Capabilities
--capability takes work_email, mobile_phone, or linkedin_url to find a value the row is missing (verify_email grades one it already has). Each writes the value, a match confidence, and the provider that produced it. mobile_phone adds --verify-phone (line type and carrier); linkedin_url gates every hit through name-and-company identity validation.
The default linkedin_url chain starts with OXYGEN's managed profile lookup (scraper.linkedin_profile_lookup): it finds the company from its domain or LinkedIn page, searches that company's people by name, and turns a member-id result into the public profile URL. Icypeas is the fallback.
A personal (non-work) address is an option of the work-email run: add --include-personal-email to --capability work_email. It uses its own single managed lane keyed on linkedin_url (the enrichment catalogue names it) and, like work_email, bills only on a hit; there is no catch-all escalation and no pattern pre-step. The winning address is graded by MillionVerifier and lands in the same cell's personal_email, never in the work address. --capability personal_email on its own still works and writes personal_email, personal_email_provider, personal_email_status, and personal_email_enriched_at — separate columns from work email.
Which provider each chain tries first and what it falls back to is listed under Waterfall defaults. Override the order for one run with --provider-order.
Previewing an enrichment
oxygen enrich-column preview <contacts-table> \
--capability work_email \
--linkedin-url-column linkedin_url \
--company-domain-column domain \
--limit 10 \
--jsonPreview calls no provider and spends 0 credits. It returns the resolved provider profile, per-provider preflight, and the credit estimate that sizes --max-credits.
Own-key fees and retries
A lane that runs on your own key bills no managed price; your provider bills the call under its own terms. Each successful own-key call pays OXYGEN's 0.05-credit platform fee (billing), even when it finds nothing. Free-on-miss provider pricing does not waive that platform fee. provider_cost_breakdown[].byok_platform_fee_credits_per_call shows the fee for each lane, and the estimates include it. The run reserves it, and a row stops before an own-key lane that its remaining --max-credits cannot pay for.
--max-credits caps OXYGEN credits only: it never limits what your provider bills your account. Cap own-key calls with an enforced provider limit (oxygen limits provider set). A live find that dispatched an own-key lane lists it in own_key_spend, and its retry_safe is false: retry_safe is true only when a re-run spends nothing (no lane errored, charged OXYGEN credits, ran on your own key, or reported unknown billing), so repeating an own-key miss calls your provider again.
A free find preview names each finder and verification leg's configured credential_mode in estimate.legs, even before a key is connected. user_api_key means your provider account and any applicable OXYGEN platform fee; managed means OXYGEN's credential. An own-key lane without a connected key is skipped at execution. billing_rules_url in previews and results links back to these definitions. The vendor may charge failed or interrupted attempts: an absent OXYGEN fee is not evidence that retrying is free.
For a one-off company lookup, oxygen find company --domain <domain> --json (its default mode is dry_run) prints the same thing as a plan: would_attempt lists, per requested field, the lanes in the order a live run would walk them, each with its estimated_credits and, where the run would not attempt it, a pre_skip reason. It is a plan, not the answer — found stays null until you run it live. The order is a fixed preference per field, not cheapest-first; a free lane behind a priced one is reached only if the priced one finds nothing.
pre_skip reasons:
missing_identity_input— the lane keys on an identity the row does not hold yet (requires_identitynames it, usuallylinkedin_urlfor a domain-only row). When another lane can resolve that identity from what the row holds, the plan adds aprerequisite: truestep ahead of the field and plans the field as if it succeeds; alinkedin_urlordomainfield you request yourself does the same for the fields listed after it.byok_only— the provider runs only on a key you connect (oxygen integrations connect <provider> --api-key <key>); OXYGEN holds no managed account for it. Itsestimated_creditsis0because nothing managed would be billed, not because it is free.credit_ceiling_reached— the lane's estimate does not fit under the--max-creditsyou passed.identity_prerequisite_unaffordable— a priced identity pre-step would eat the budget the requested field itself needs.provider_not_selected— table previews only: the lane belongs to a provider you left out of--providers, so the run never reaches it. Itscreditsshow what adding that provider would cost.check_technologies_missing—technology_checkwas requested without--check-technologies.
To ask yes/no questions about a company's stack, request the technology_check field and name the technologies. It reads the detected stack, so the technology lookup runs and bills as usual:
oxygen find company --domain acme.com --fields technology_check --check-technologies hubspot,salesforce --jsonSome company lanes cost nothing at all (a derived field reads a source field's payload for 0 credits). oxygen find company ... --mode live --max-credits 0 is therefore a real request, not a rejected one: it runs only the zero-credit lanes, skips every priced one as credit_ceiling_reached, and bills nothing. Use it to ask "answer this only if it is free". The person capabilities (find email, find phone, find linkedin, find personal-email, find person) have no free lane, so they still require a ceiling above 0.
One-off person lookups
oxygen find runs the same waterfalls for one person without a table. Every command previews for free (dry_run) until you pass --mode live with a --max-credits ceiling.
oxygen find person --linkedin-url https://www.linkedin.com/in/example --json
oxygen find personal-email --linkedin-url https://www.linkedin.com/in/example --jsonfind person returns one profile block: name, current company and its LinkedIn URL, job title and start date, education, headline, bio, location, and follower count. It runs Blitz first and the managed LinkedIn lookup only when Blitz has no profile, each billed only on a hit; follower count comes only from the LinkedIn lookup. Given an email, or a name with --company-domain or --company-name, it first resolves the LinkedIn URL through the LinkedIn-URL waterfall, priced in the preview as a prerequisite. A Sales Navigator link (--sales-navigator-url) resolves on your own HarvestAPI key. find personal-email runs the personal-email lane above and needs --linkedin-url.
A live person find (email, personal-email, phone, linkedin, person) has a 50-second time limit. When too little time is left for its next provider or check, it starts no more of them and marks the steps it did not reach deadline_reached: a value already found is returned without those checks, and otherwise the outcome is indeterminate, never a no-match. Run the same find again to continue: answers managed providers already gave for that input are reused from your workspace's lead cache for 0 credits.
Per-cell provenance
An enrichment cell records the whole call trace — source is the tool that won:
{
"value": "[email protected]",
"source": "leadmagic.email_finder",
"confidence": "verified",
"attempts": [
{ "provider": "icypeas", "operation": "email_discovery", "status": "no_result", "latency_ms": 412 },
{ "provider": "leadmagic", "operation": "email_finder", "status": "success", "latency_ms": 380, "billing": { "credits_used": 12.25 } }
]
}Attempt status is success, no_result, skipped, error, or incomplete_async (the provider took the request async and is polled later — not a failure). A skipped attempt carries a reason such as missing_input or credit_ceiling_reached. Inspect a cell with oxygen cells inspect.
A company lane whose answer describes a different company is refused: the attempt is no_result with reason identity_mismatch (shown as Wrong company), or identity_unverifiable (Identity unverified) when a search-shaped lane returned no identity to check. Its identity_match holds the evidence, for example { "matched": false, "requested": "acme.com", "returned": "tata.com" }. The refused value is never written, the waterfall moves on to the next lane, and billing follows the lane's own rule unchanged: refusing an answer changes what is written, not what the provider charged. A per_attempt lane bills the attempt as always; an on_hit lane bills a refused answer the provider did return and releases only a provider miss (found: false). The cell detail in the grid and in the MCP cell widget shows the label with the requested and returned identity.
Cost behavior
Billing depends on the intent. Managed work-email and phone finder lanes charge only when they return a value; verification can still cost credits. Company-field waterfalls bill their Blitz lanes (domain ↔ LinkedIn URL and the company profile) only on a hit, and every other managed lane on each attempt, whether or not it finds anything. On the managed LinkedIn-URL waterfall a URL found that matches the person costs a flat 5 credits (9.2 while OXYGEN’s managed LinkedIn rail runs on HarvestAPI), whichever default lane finds it (a lane you add with --provider-order bills its own price, on a hit), and a row where no lane finds one costs nothing — see tool costs. --max-credits is required on a live run and caps OXYGEN credits, including own-key platform fees; vendor-account spend remains separate.
Guessed-address checks on work email. Before any finder runs, a work_email row that has a person's name (full, or first and last) and a company domain but no LinkedIn profile URL tries up to eight guessed addresses at that domain (first.last, firstlast and similar) through the email verifier. Each check bills at the verifier's price, 0.225 credits on OXYGEN's managed MillionVerifier lane, whether or not the guess is valid. So these checks alone can cost up to 1.8 credits on a row where no email is found. The first guess the verifier marks valid is written to the cell, and no finder is called. The free preview and dry run quote these checks. To skip them, pass --email-pattern-validation disabled to oxygen enrich-column preview and oxygen enrich-column run. Switching Email verification off on the column skips them too, along with the check on every found address. On the default auto profile, a row with a LinkedIn profile URL never runs them. Forcing --email-waterfall-profile name_domain or first_last_domain runs them on every row with a name and a domain.
--allow-premium-lanes opts an enrich-column run into managed lanes priced above 50 credits per call on a capability that bills every attempt; without it such a lane is pre-skipped as premium_lane_not_opted_in, so an unattended run cannot bill-shock.
No enrich-column capability bills every attempt today. The finder lanes of work_email, personal_email, mobile_phone, and linkedin_url all bill only on a hit, so a lane's list price is not spend, and their expensive tail lanes run by default: passing --allow-premium-lanes changes nothing for them. A lane that finds nothing costs nothing, but a row that ends with no value can still cost credits. The guessed-address checks above bill per check. A value that a finder returns and verification then rejects (an invalid email, or a number the optional phone check marks invalid) still bills that finder and the check. The waterfall then moves on, so more than one lane can charge on a row. Bound real spend with --max-credits. Company-field enrichment has no premium gate: bound it with --max-credits and the preview's per-lane credits.
oxygen enrichment catalog --json lists every enrichment column kind and preset with a per-row credit estimate before you add anything — see it before picking a capability by hand.
Running live
oxygen enrich-column run <contacts-table> \
--capability work_email \
--linkedin-url-column linkedin_url \
--company-domain-column domain \
--limit 100 \
--approved \
--max-credits 100 \
--jsonCompany field enrichment has its own helper. run defaults to live mode, so it needs --approved and --max-credits:
oxygen companies enrich preview <accounts-table> --missing-fields headcount,industry --limit 10 --json
oxygen companies enrich run <accounts-table> --missing-fields headcount,industry --limit 100 --approved --max-credits 100 --jsonThe table preview carries the same would_attempt plan, for one representative row shaped like the identity columns that set the per-row ceiling (lane_plan_identity). Each lane adds credits, its price per attempt from the live price table (lane_pricing.priced_from), or null with price_status: "unavailable" when no honest price can be derived, and billed: per_attempt bills every attempt, hit or miss, while on_hit (the BlitzAPI company lanes) releases a miss. A lane of an own-key-only provider you did not select keeps pre_skip: "byok_only" with provider_not_selected: true. providers_without_lane names the selected providers that offer nothing for a field, so the run never asks them for it. estimated_credits_per_row and estimated_credits stay the ceiling to approve.
Company field groups as columns
A company preset adds one paid lookup column plus free columns for the fields it fills. Creating the columns costs 0 credits; they bill when you run them, by the company-field rules above.
oxygen columns add <accounts-table> --preset company_funding --json| Preset | Fills |
|---|---|
company_funding | Total raised, latest round, number of rounds, and investors |
company_revenue | An annual revenue estimate — no source reports exact revenue for private companies |
company_hiring | Open roles and how many there are |
company_tech_stack | Technologies the company runs |
company_lookalikes | Up to 25 similar companies per seed company, kept as a separate list from the seed; reads the seed's domain column (--input company_domain=<column> if it is not detected) |
oxygen tools search --for-table <accounts-table> --json prices each preset per row before you add it, and oxygen columns run <accounts-table> <column> --dry-run --json previews a run before you approve it.
Related
- Provider reference — the default and fallback provider for every chain.
- Columns — enrichment columns and materialized outputs.
- Cells — inspecting per-attempt history.