OXYGENOxygen/ Docs
Providers

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, personal_email, mobile_phone, or linkedin_url. 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.

personal_email finds a person's personal (non-work) address through its own single managed lane keyed on linkedin_url (the enrichment catalogue names it). Like work_email, it bills only on a hit; there is no catch-all escalation and no pattern pre-step. The winning address is graded by MillionVerifier, and the result lands in 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 \
  --json

Preview 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. Once the BYOK platform fee applies (billing), each successful own-key call pays 0.05 OXYGEN credits, 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_identity names it, usually linkedin_url for a domain-only row). When another lane can resolve that identity from what the row holds, the plan adds a prerequisite: true step ahead of the field and plans the field as if it succeeds; a linkedin_url or domain field 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. Its estimated_credits is 0 because nothing managed would be billed, not because it is free.
  • credit_ceiling_reached — the lane's estimate does not fit under the --max-credits you 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. Its credits show what adding that provider would cost.

Some 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) have no free lane, so they still require a ceiling above 0.

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": "hunter", "operation": "email_finder", "status": "no_result", "latency_ms": 412 },
    { "provider": "leadmagic", "operation": "email_finder", "status": "success", "latency_ms": 380, "billing": { "credits_used": 1 } }
  ]
}

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 charge every managed lane they attempt, whether or not it finds anything. The managed LinkedIn-URL waterfall does the same until the 2026-09 repricing date; from it a URL found that matches the person costs a flat 5 credits, whichever lane finds it, 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.

Managed lanes priced above 50 credits per call are pre-skipped unless you pass --allow-premium-lanes, so an unattended run cannot bill-shock. They appear in the trace as skipped with reason premium_lane_not_opted_in.

This gate does not apply to work_email, personal_email, or mobile_phone. Those bill only on a hit, so a lane's list price is not spend — a miss costs nothing and at most one lane ever charges per row. Their expensive tail lanes therefore run by default, and passing --allow-premium-lanes changes nothing for them; gating them on list price once removed the whole tier-2/3 tail and made the cheapest-first order pointless. Real spend on those intents is bounded by charge-on-hit plus --max-credits. The gate governs one capability: linkedin_url, whose leadmagic.email_to_profile lane bills 122.5 credits on every attempt, and only until the 2026-09 repricing date, when linkedin_url starts billing on a hit as well.

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 \
  --json

Company 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 --json

The 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.

  • Provider reference — the default and fallback provider for every chain.
  • Columns — enrichment columns and materialized outputs.
  • Cells — inspecting per-attempt history.

On this page