> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revyops.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Statuses

> Every source keeps its own result for an email, and one rule picks the result Email Status shows.

An email can be checked by several tools over time: Million Verifier says `catchall`, BounceBan later says `valid`, a sequencer reports a bounce, a rep corrects it by hand. RevyOps keeps one result per source for each email, with its own dates, and shows one of them as the contact's **Email Status**.

## Which result shows

Email Status shows, in this order:

1. `unsubscribed`, from any source. An unsubscribe on one email does not affect the person's other emails.
2. Otherwise the newest **live** result: a real send, bounce, call or hand edit.
3. Otherwise the newest **check**: a verifier's opinion.

An email with no results keeps the Email Status it already had. If that status is `bounced` or `unsubscribed`, the first new result first saves it as a live result from the `before_store` source, dated just before that result. A verifier check never hides it; a newer live result, such as a hand edit, replaces a `bounced` one, and `unsubscribed` always wins. `before_store` cannot be named as a source, to write or to clear. Any other status from before is replaced by the first new result.

Results arrive when someone writes them: a hand edit in the app, or a call to the API or MCP that names the source, such as your verification script naming `millionverifier` or a workflow naming `instantly`. Integrations do not write results by themselves yet.

For `jo@acme.com`:

| Result written | Email Status shows | Why |
| - | - | - |
| Million Verifier `catchall`, checked 10 Oct | `catchall` | The only result |
| BounceBan `valid`, checked 11 Oct | `valid` | The newest check |
| A rep sets `valid` by hand on 12 Oct | `valid` | A hand edit is live |
| Instantly `bounced`, 13 Oct | `bounced` | The newest live result; a bounce is always live |
| Million Verifier `valid`, checked 5 Jan | `bounced` | A check never beats a live result |
| Instantly `unsubscribed`, 6 Jan | `unsubscribed` | Unsubscribed beats everything |
| Million Verifier `valid` again, checked 1 Feb | `unsubscribed` | Million Verifier's last check moves to 1 Feb, its status changed date stays 5 Jan |

## Values

Every source writes one of seven values: `valid`, `invalid`, `risky`, `catchall`, `unknown`, `bounced` and `unsubscribed`. A tool's own word, such as `ok` or `accept_all`, is mapped to one of them before it is written; the word itself is kept beside the result as `raw_value`.

`bounced` is any bounce, hard or soft. A bounce is always live, so it outranks every later verifier check until a send, a hand edit or another live result replaces it. `bounced` and `unsubscribed` are always stored as live, whatever the writer sends.

## The two dates

| Date | API field | Moves when |
| - | - | - |
| Last checked | `observed_at` | The source reports a result for the email, even the same value as before |
| Status changed | `status_changed_at` | The source reports a different value from its previous result |

Both come from the source. A result dated earlier than the one the same source already has is ignored, without an error, so a retried webhook cannot undo a newer check.

## Sources

A source is either one RevyOps knows (`millionverifier`, `bounceban`, `leadmagic`, `apollo`, the sequencer and CRM integrations, `revyops` for edits on the record page, `api` for a write that names no source), or one the workspace adds itself. To write results from a tool RevyOps does not list, add it once as a source under [Activities & Scoring](https://docs.revyops.com/user-guide/activities-and-scoring.md) or with the `create_source` MCP tool, for example with the key `acmeverify`. The integration names and `api` cannot be added as a workspace's own source. `revyops` is kept for hand edits on the record page: the API and MCP cannot name it as a source, and doing so returns a 400. A clear without a source still removes every result, the hand edit included.

## Writing a status

* **Contacts API.** `PATCH /public/v2/contacts/{contact_id}`, and `POST` or `PUT` on `/public/v2/contacts`, take `email_status` with three optional keys: `email_status_source` (the source, `api` when left out), `email_status_checked_at` (when the source checked the email, now when left out) and `email_status_live` (false when left out). `email_status_checked_at` needs a timezone, such as `2026-10-10T08:00:00Z` or `2026-10-10T10:00:00+02:00`; a time without one returns a 400. With `email_status_source`, `email_status` must be one of the seven values and any other word returns a 400 that lists them. Without it, the older behavior stays: any letter case and verifier spellings such as `Catch-All` or `deliverable` are accepted and stored as the `api` source. Sending back the value Email Status already shows, with none of the three keys, changes nothing, so reading a contact and writing it back unchanged does not add an `api` result. With any of the three keys the result is written, so `{"email_status": "invalid", "email_status_live": true}` records a live `api` result even when a check already shows `invalid`. A `POST` or `PUT` whose body has more than one of `email`, `linkedin_url` and `phone` ignores `email_status` and the three keys; send the status afterwards with `PATCH` on the email contact.
* **MCP.** `update_contact` takes `email_status`, `email_status_source` and `email_status_checked_at`; the source and the check time, with its timezone, are required whenever a status is sent, because the time has to come from the tool. An empty `email_status` removes only that source's result.
* **Record page.** Setting Email Status by hand writes a live result from the `revyops` source.
* **Custom fields.** A custom field named `email_status`, `Contact Email Status` or `Email Status` is written as a check from the `api` source. Sending the same value again changes nothing while the `api` result still holds it; after the email address changes, the same value is written again.

Only a contact with an email address takes an email status. On a LinkedIn or phone contact the custom field above is ignored, and so is an `email_status` sent without `email_status_source`; with a source the request returns a 400.

## Removing results

| You do | What happens |
| - | - |
| Change the contact's email address | Every result of the old address is removed and Email Status empties, unless the same request sends a new status |
| Send an empty `email_status` without a source | Every source's result is removed and Email Status empties |
| Send an empty `email_status` with `email_status_source` | Only that source's result is removed, and Email Status shows the next result by the rule above, or empties when none is left. A source with no result changes nothing; an unknown source returns a 400 |
| Clear Email Status on the record page | Every source's result is removed and Email Status empties |
| Delete the Email Status column under Manage Columns | Every contact's Email Status empties and every source's result in the workspace is removed |

## Statuses from before per-source results

A contact whose Email Status was set before per-source results existed has an empty `email_statuses` list: there is no record of which tool set it. The list fills as sources write. Only `bounced` and `unsubscribed` are kept as a `before_store` result when the first new result arrives, as described under [Which result shows](#which-result-shows); a `before_store` entry then appears in the list like any other source.

## Reading results

`GET /public/people/{person_id}` returns a `statuses` list on every item in `emails`, and `GET /public/v2/contacts/{contact_id}` returns the same list as `email_statuses`. The app's person and contact pages show the same list beside Email Status, where `before_store` reads as **Already on record**; see [Seeing each tool's result](https://docs.revyops.com/user-guide/contacts.md#seeing-each-tools-result). Each entry has `source`, `status`, `raw_value`, `live`, `observed_at` and `status_changed_at`, newest check first. The MCP `get_contact` tool returns the same list.

## Phone statuses

A workflow's [Verify email step](https://docs.revyops.com/user-guide/workflows.md#verify-email-step) writes results the same way, under `millionverifier` or `bounceban`, through the workspace's status mapping.

Phones get a status too, from a workflow's [Verify phone step](https://docs.revyops.com/user-guide/workflows.md#verify-phone-step), which checks them with [Twilio](https://docs.revyops.com/integrations/twilio.md). Each check is kept per source like an email result, and the contact shows the newest one as **Phone Status** (`valid`, `unreachable`, `invalid` or `unknown`) and **Phone Line Type** (`mobile`, `landline`, `voip` or `other`). Both are read-only, show on the phone's row of the person and contact pages with who checked it and when, and can be filtered on in the People and Contact lists.

## Filtering by result

The People and Contact lists filter by each tool's own result through the **Email Status Result** group; see [Filtering by each tool's email status](https://docs.revyops.com/user-guide/filters-and-saved-views.md#filtering-by-each-tools-email-status). In the filter API and the MCP `query_entity` and `save_view` tools, these results are the `email_status_result` entity, reached from a contact or person through the `email_status_results` relation, with the fields `source`, `status`, `raw_value`, `live`, `observed_at` and `changed_at` (the result's `status_changed_at`).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.