Email verification
Check whether addresses you already have are safe to send to, including catch-all domains.
Verification grades an address you already have: is it safe to send to? Finding an address you don't have is a different chain — see Waterfalls.
One verifier chain serves every surface — the verify_email enrichment column, oxygen verify email, the oxygen_verify_email MCP tool, and POST /api/cli/verify/run — so the same address cannot get a different verdict, or a different price, depending on where you asked. The chain's current members are listed under Waterfall defaults.
Verdicts
| Verdict | Meaning | Send to it? |
|---|---|---|
valid | A verifier confirmed the mailbox exists. | Yes |
invalid | The mailbox does not exist, or the address is disposable, a spam trap, or do-not-mail. | No |
catch_all | The domain accepts mail for every address, so no verifier can confirm this specific mailbox. | Judgement call — see below |
unknown | No verifier could answer. This is an absent answer, not a bad address. | Don't drop these; re-run |
The two that get misread are catch_all and unknown. Filtering "everything that isn't valid" throws away good contacts: unknown usually means a verifier had a bad moment, and catch_all is extremely common on well-run corporate domains.
Catch-all domains
A catch-all (or "accept-all") domain answers yes to every address, so probing anything@company.com succeeds whether or not the mailbox is real. A generalist verifier can only flag this.
When that happens, Oxygen automatically escalates the address to a catch-all specialist that resolves a large share of them, including addresses behind secure email gateways. You do not configure this; it is the default chain.
Two rules keep the cost sane:
- Escalation fires only on a
catch_allverdict — never speculatively, and never onunknown(an inconclusive cheap pass is not evidence of a catch-all domain). - If the specialist cannot resolve it either, the address keeps
catch_all. Oxygen never upgrades a catch-all to verified because the escalation was unavailable.
For addresses that stay catch_all, the honest play is to send a small watched batch first, or confirm the person independently — verification alone cannot clear them.
Previewing readiness
The free preview checks the selected route without verifying the address. Read catch_all_escalation_available: false reports a known blocker and a next_action; null with catch_all_escalation_availability: "unknown" means Oxygen could not check availability, so retry the preview. true means the snapshot found no blocker. It does not prove a fresh vendor wallet balance or guarantee that a later live call will succeed. A tool labelled runnable has the same limit.
Use the specific tool's availability and the verification preview for the selected managed or BYOK route. The integrations catalog describes connection options. A configured route does not authorize spending: live execution checks the provider, eligibility and your approved ceiling again.
Cost, and sizing the ceiling
Cost is not flat per address. Every address pays the first pass; only catch-all addresses additionally pay the escalation, which is several times more. A ceiling sized as "N addresses × first pass" runs out as soon as a few turn out to be catch-all.
Run the free preview first. It returns real numbers:
oxygen verify email a@acme.com b@acme.com --json{
"estimate": {
"first_pass_credits_per_email": 2.245,
"catch_all_escalation_credits_per_email": 17,
"min_credits": 4.49,
"max_credits": 38.49,
"recommended_max_credits": 38.49
}
}Pass recommended_max_credits (the estimate's worst case: every verifier plus an escalation on every address) as --max-credits. It is a ceiling, not a charge — you are billed for what actually runs, so over-reserving costs nothing.
oxygen verify email a@acme.com b@acme.com --mode live --max-credits 38.49 --jsonIf the ceiling runs out mid-batch, verification does not fail. The remaining catch-all addresses come back unescalated with escalation_skipped: budget_exhausted — a budget signal, not a provider outage. Re-run those with a higher ceiling.
A catch_all whose escalation did not run is recorded as unconfirmed, not as verified. The address keeps catch_all; treat it as an address nobody has cleared yet, and re-run it when the reason below says re-running will help.
Every skipped escalation carries a next_action sentence — on the address, and again at the top of the envelope for the most serious one in the batch.
escalation_skipped | Cause | Fix |
|---|---|---|
budget_exhausted | Remaining ceiling could not cover an escalation, so it was never dialled. Nothing was charged for it. | Re-run with recommended_max_credits. |
escalation_unavailable | The escalation could not run or could not answer. A known account refusal or staff hold can stop it before a provider call. Read escalation_unavailable_reason. | See the next table. |
not_configured | Escalation was explicitly disabled. | Nothing, if intended. |
escalation_unavailable_reason | Cause | Fix |
|---|---|---|
provider_account_dry | The account the escalation bills against has no provider credits left. On the managed lane that is Oxygen's account with the vendor — not your workspace credit balance — and you were not charged for the escalation. | Nothing on your side: Oxygen staff have to top the managed account up, and re-running before that fails the same way. Re-run the affected addresses after the top-up. On a BYOK connection it is your own vendor account: top it up, then re-run. |
provider_account_held | Oxygen staff has placed a hold on the managed account. The escalation was not charged; an earlier first pass may still have cost credits. | Oxygen staff must review and release the hold. Funding the account or waiting for a retry interval alone does not release it. Keep the address unconfirmed. |
not_configured | No usable credential for the escalation provider on this workspace. Nothing was charged. | Connect your own key (oxygen integrations connect bounceban --api-key <key>), or ask Oxygen support to enable the managed lane. |
provider_error | The provider was dialled and returned an error or timed out. The failed leg is not counted in credits_used. | Usually transient — re-run later. The attempt's provider_message holds what the vendor actually said. |
Each attempt in the trace carries a plain-sentence message plus the vendor's own provider_message; message is the one that explains the outcome.
Verifying a whole table
Add an Email verification column pointed at the column holding your addresses. It runs the same chain, including the automatic catch-all escalation, and writes the verdict into the cell so you can filter and segment on it.
In the app, open a table and click + Add column → Email verification. It sits with Work email, Mobile phone, and LinkedIn URL. If your table has a column named email or work_email, Oxygen maps it automatically; otherwise pick it in the column editor.
From the CLI, the same click is one command — it creates the column and stops there. It costs 0 credits and verifies nothing; the column sits ready until you run it.
oxygen columns add <table> --capability verify_email --jsonThe column comes back as kind: enrichment with a jsonb cell, labelled Email Verification with the key email_verification, already pointed at the same MillionVerifier → BounceBan chain the picker wires up. --label and --key override the defaults. Use this whenever you want the column defined now and run later — enrich-column run refuses without --approved and --max-credits, and creates nothing when it refuses.
To verify, preview for free first — the preview returns recommended_max_credits, which is the number to pass:
oxygen enrich-column preview <table> --capability verify_email --json
oxygen enrich-column run <table> --capability verify_email \
--max-credits <recommended> --approved --json--email-column <key> names the source column explicitly when auto-detection picks the wrong one.
Do not hand-build a single-provider action column for this — that skips the escalation, so every catch-all row comes back unresolved. If you searched the column picker for a verifier and found a provider operation, that is the shape this warning is about: use the Email verification column instead.