This is the full developer documentation for EmailShield # EmailShield Docs > Email verification and list cleaning, explained in plain words. Start here if you've never verified a list, or jump straight to the API. ## Every cold email setup has four parts [Section titled “Every cold email setup has four parts”](#every-cold-email-setup-has-four-parts) EmailShield is part 4: the last check before you send. It looks at every address on your list, asks its mail server whether the mailbox exists — **without sending an email** — and tells you which ones are safe to send to. [Part 1 Infrastructure ![](/partners/maildeck.png) Maildeck Domains and inboxes you send from ](/stack/maildeck/)[Part 2 Sequencer ![](/partners/instantly.png)![](/partners/plusvibe.png) Instantly, PlusVibe… Sends emails and follow-ups ](/stack/sequencer/)[Part 3 Leads ![](/partners/leadsonar.svg) LeadSonar The right people and their emails ](/stack/leadsonar/)[Part 4That's us Verification ![](/favicon.png) EmailShield Removes bad addresses first](/verify/upload-a-list/) ## Start here [Section titled “Start here”](#start-here) [Cold email in 5 minutes ](/getting-started/cold-email-basics/)The four parts, and why verifying your list protects your domains. [Your first list in 10 minutes ](/getting-started/quickstart/)Sign up, upload a CSV and download the clean version. [Credits and plans ](/getting-started/credits-and-plans/)What a credit buys, what's free, and what's refunded. [Reading your results ](/results/reading-your-results/)Safe to send, risky and invalid — and what to do with each. ## Understand every answer [Section titled “Understand every answer”](#understand-every-answer) [Every status explained ](/results/statuses/)Valid, catch-all, role, disposable, spam trap, no mail server and the rest. [Catch-all addresses ](/results/catch-all/)Domains that accept everything, and how EmailShield rates them. [Why some results are unknown ](/results/unknowns/)When a mail server won't answer — and why you're not charged for it. [How verification works ](/results/how-verification-works/)What happens to an address between upload and verdict. ## Build it into your own tools [Section titled “Build it into your own tools”](#build-it-into-your-own-tools) REST API Verify one address or 50,000 per call, poll the job, page through results, get a signed webhook when it’s done. One API key, JSON in and out. [API overview →](/api/overview/) For AI agents Plain-text versions of these docs for assistants and agents, and the rules an agent should follow before it spends credits. [EmailShield for AI agents →](/ai-agents/) ## Need a hand? [Section titled “Need a hand?”](#need-a-hand) Use the chat bubble in the corner, join the community on [Slack](https://slack.emailshield.co), or see [Get help](/help/getting-help/). # Cold email in 5 minutes > The four parts of every cold email setup, and why verification is the last check before you send. New to cold email? This page is the whole picture in five minutes. Every setup that works has the same four parts, and **EmailShield is part 4**: the last check before anything is sent. [Part 1 Infrastructure ![](/partners/maildeck.png) Maildeck Domains and inboxes you send from ](/stack/maildeck/)[Part 2 Sequencer ![](/partners/instantly.png)![](/partners/plusvibe.png) Instantly, PlusVibe… Sends emails and follow-ups ](/stack/sequencer/)[Part 3 Leads ![](/partners/leadsonar.svg) LeadSonar The right people and their emails ](/stack/leadsonar/)[Part 4That's us Verification ![](/favicon.png) EmailShield Removes bad addresses first](/verify/upload-a-list/) | Part | What it does | Who provides it | | --------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | **1. Infrastructure** | The domains and inboxes your emails are sent from, set up and warmed up so they can send safely. | [Maildeck](/stack/maildeck/), our sister product | | **2. Sequencer** | Sends your emails and follow-ups on a schedule, spread across your inboxes. | A tool you sign up for yourself, such as Instantly or PlusVibe | | **3. Leads** | Finds the right people and their work email addresses. | [LeadSonar](/stack/leadsonar/), our sister product | | **4. Verification** | Checks every address before you send and removes the ones that would bounce. | **EmailShield — that’s us** | In one line: **LeadSonar finds the people, EmailShield cleans the list, your sequencer sends the emails, and Maildeck gives it all somewhere safe to send from.** ## Why verification can’t be skipped [Section titled “Why verification can’t be skipped”](#why-verification-cant-be-skipped) When you email an address that doesn’t exist, the email **bounces**: it comes back undelivered. Email providers like Google and Microsoft watch your bounce rate. Lots of bounces tells them you’re sending to a list nobody checked, which is what spammers do. Your **sender reputation** drops, more of your emails land in spam, and in bad cases your domain ends up on a **blacklist** that many mail servers refuse outright. The domain is the expensive part to lose: it took weeks of warm-up before it could send. **Verifying a list costs far less than replacing a burned domain.** Keep bounces under 5% If a campaign’s bounce rate goes above about 5%, pause it and clean the list before sending more. Most teams aim for under 2%. ## Why every list needs it, even a good one [Section titled “Why every list needs it, even a good one”](#why-every-list-needs-it-even-a-good-one) * **People change jobs every month.** An address that was valid when the list was built can be gone by the time you send. Verify **the week you send**, not when you build the list. * **Every bad address wastes a sending slot.** Each inbox can only send a few dozen cold emails a day. A slot spent on an address that bounces is a slot not spent on someone who could reply. * **Some addresses are traps.** Spam traps and abuse desks exist to catch senders who don’t check their lists. EmailShield flags the ones it recognises so you can leave them out. Words you’ll see a lot A **bounce** is an email that comes back undelivered. A **catch-all** domain accepts mail for every address, so a single mailbox can’t be confirmed either way. **Sender reputation** is how much the big email providers trust your domain and inbox. The [Glossary](/help/glossary/) has the rest. ## What to read next [Section titled “What to read next”](#what-to-read-next) * [Your first list in 10 minutes](/getting-started/quickstart/) — upload a list and download the clean version. * [Reading your results](/results/reading-your-results/) — what *safe to send*, *risky* and *invalid* mean. * Maildeck’s guide covers infrastructure in depth: [docs.maildeck.co](https://docs.maildeck.co/getting-started/cold-email-basics/). # Credits and plans > What one credit buys, what's free, what's refunded, and how to choose a plan or a top-up. EmailShield runs on **credits**. **One credit checks one address.** You only pay for answers: if we can’t get one, the credit comes back. ## What costs credits [Section titled “What costs credits”](#what-costs-credits) | Action | Cost | | ----------------------------------------------------------- | ---------------------------------------------- | | Verifying an address (upload, paste, single or API) | **1 credit** | | Duplicate addresses in the same list | **Free** — removed before charging | | Addresses that aren’t valid email syntax | **Free** — marked invalid, never charged | | Results that come back **Unknown** | **Refunded** when the job finishes | | [Full Scan](/tools/full-scan/) of one domain, address or IP | **Free** | | Full Scan in bulk (a file of domains) | 1–5 credits per target, depending on the check | ### How the refund works [Section titled “How the refund works”](#how-the-refund-works) A job takes credits for every address up front, when you submit it. When it finishes, every address that came back **Unknown**, **Still resolving** or **Not verified** is refunded automatically. If a job fails outright, everything it didn’t answer is refunded. Every other result is a real answer and is charged — including **invalid**, **catch-all**, **role** and **disposable**. Knowing an address will bounce is exactly what saves your domain. ## Free trial [Section titled “Free trial”](#free-trial) * **7 days** and **5,000 credits**, no card needed. * Another **35,000 bonus credits** are waiting in your account. They unlock with your first plan or top-up. * The trial includes the dashboard, uploads and Full Scan. **API access starts with any purchase.** ## Plans [Section titled “Plans”](#plans) Credits are added every 30 days on your billing date. Every plan includes API access. | Plan | Price | Credits per month | API keys | API verifications | | ------------- | -------------- | ----------------- | -------- | ----------------- | | **Starter** | $19 / month | 10,000 | 1 | 10,000 / day | | **Growth** | $29 / month | 50,000 | 3 | 50,000 / day | | **Plus** | $49 / month | 100,000 | 3 | 100,000 / day | | **Pro** | $149 / month | 500,000 | 10 | Unlimited | | **Scale** | $249 / month | 1,000,000 | 10 | Unlimited | | **Scale Max** | $1,797 / month | 10,000,000 | 10 | Unlimited | Need more than that, or something custom? Ask us in the chat. Upgrades apply instantly: the new credits are added to your balance straight away and your API limits reset on the spot. The dashboard itself is never rate-limited. ## Top-ups [Section titled “Top-ups”](#top-ups) One-time credit packs. They work with or without a plan, and **top-up credits never expire.** | Credits | Price | | ---------- | ------ | | 10,000 | $19 | | 50,000 | $29 | | 100,000 | $49 | | 500,000 | $159 | | 1,000,000 | $269 | | 5,000,000 | $1,149 | | 10,000,000 | $1,979 | Your first top-up also moves a trial account to pay-as-you-go, unlocks API access and releases the 35,000 bonus credits. The more credits you’ve bought in total, the higher your API allowance: 100,000+ gives 3 keys and 50,000 verifications a day, and **500,000+ means unlimited API**. ## How many credits do you need? [Section titled “How many credits do you need?”](#how-many-credits-do-you-need) A sequence usually sends each person about three emails. A rough rule: **addresses to verify per month ≈ emails you plan to send per month ÷ 3**, plus a little extra because some will come back invalid. If your inboxes send 30,000 cold emails a month, you need roughly 10,000–12,000 credits. ## Changing or cancelling [Section titled “Changing or cancelling”](#changing-or-cancelling) Manage everything under **Plans & Billing** in the app. Payment happens on a secure checkout page; after your first payment we ask once for your business details for invoices. If you cancel, or a plan or trial ends, you keep **1,000** of your plan credits and **all of your top-up credits**. The rest of your plan credits are held, not lost — subscribing again brings them back. # Your first list in 10 minutes > Create an account, upload a list, and download the addresses that are safe to send to. By the end of this page you’ll have verified a real list and downloaded the addresses that are safe to send to, ready for your sequencer. 1. **Create your account** at [app.emailshield.co](https://app.emailshield.co). Sign up with email, Google or Discord. Every new account starts a **7-day free trial with 5,000 credits** — one credit checks one address. No card needed. 2. **Tell us who you are.** Trial accounts fill in a short profile first: your company, an optional website and a phone number. It keeps free credits away from throwaway accounts. We check the number is real for its country; there’s no SMS code. 3. **Prepare your file.** A `.csv` or `.txt` file with one address per row. Keep your other columns (first name, company…) — they come back in the export. EmailShield finds the email column on its own if the header is something like *Email*, *Work Email* or *Email Address*. Tip Only one column should be called “email”. If your file has both *Email* and *Personal Email* with addresses in them, the upload is refused — delete the one you don’t want to check. 4. **Upload it.** On the **Dashboard**, open the **Upload** tab and drop in the file. Duplicates are removed automatically (ignoring capital letters) and addresses that aren’t valid email syntax are marked invalid straight away — **neither costs a credit**. 5. **Wait for the job to finish.** You’ll land on the job’s results page and can watch it fill in. Small lists take minutes; big lists take longer, because mail servers are asked at a polite pace. You can close the tab — the job keeps running, and you can ask for an email when it’s done under **Profile → Notifications**. 6. **Download the clean list.** On the **Overview** tab, export the **Safe to send** group. That’s your list. [What the other groups mean →](/results/reading-your-results/) ## Checking just a few addresses [Section titled “Checking just a few addresses”](#checking-just-a-few-addresses) The Dashboard also has a **Paste** tab (paste addresses separated by new lines, commas or semicolons) and a **Single** tab for one address. Both create a small job and take you to its results, the same as an upload. ## Next [Section titled “Next”](#next) * [Reading your results](/results/reading-your-results/) — safe to send, risky and invalid. * [Load into your sequencer](/stack/sequencer/) — from export to campaign. * [Credits and plans](/getting-started/credits-and-plans/) — when the trial runs out. # Limits and errors > API allowances, rate-limit headers and every error the API returns, with what to do about each. ## Allowances [Section titled “Allowances”](#allowances) Allowances count **addresses verified through the API**, per account, per UTC hour and UTC day. The whole submission has to fit: a call that would go over is refused in full, never half-accepted. Polling and reading results are never counted. [Allowance per plan →](/api/overview/#how-much-you-can-send) A `429` response carries the numbers: | Header | | | ------------------------------------------------------ | ------------------------------------------------------------------- | | `X-RateLimit-Limit-Hour`, `X-RateLimit-Remaining-Hour` | This hour (limit `0` and remaining `unlimited` when there’s no cap) | | `X-RateLimit-Limit-Day`, `X-RateLimit-Remaining-Day` | Today (UTC) | | `X-RateLimit-Reset` | When the next window starts (ISO 8601) | Separately, each IP address can make about **20 requests per second** to the API. Poll every 10–30 seconds; there’s no benefit in polling faster. ## Errors [Section titled “Errors”](#errors) Errors come back as JSON with a `detail` field — usually a sentence, sometimes an object with a `code`. ```json {"detail": "Invalid API key"} ``` | Code | Meaning | What to do | | ----- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `400` | Nothing to verify after removing duplicates, or too many addresses | Check the request | | `401` | Missing or invalid API key (`Missing API key. Include X-API-Key header.` / `Invalid API key`) | Check the `X-API-Key` header; the key may have been revoked | | `402` | Not enough credits, or no active plan with credits | Top up or renew under **Plans & Billing** | | `403` | The account has no API access yet (trial), the key lacks the `write` scope, or the endpoint is dashboard-only | Make a purchase to unlock the API; use a read/write key | | `404` | Job or key not found (or not yours) | Check the ID | | `408` | `/v1/verify/single` didn’t get an answer within 30 seconds; the credit is refunded | Retry, or use a bulk job | | `422` | Invalid request: a malformed address, more than 50,000 addresses, or a rejected `webhook_url` (the reason is in `detail`) | Fix the request; nothing was charged | | `429` | Over your hourly or daily API allowance | Wait for `X-RateLimit-Reset`, or upgrade — new limits apply instantly | | `503` | New jobs are briefly paused (`"code": "submissions_paused"`, *Technical maintenance*) | Retry after the `Retry-After` header. Running jobs are unaffected and nothing was charged | | `500` | The job couldn’t be queued; any credits taken are refunded | Retry; if it keeps happening, contact us | A `429` example: ```json {"detail": "This submission of 20,000 verification(s) exceeds your plan's daily API budget of 10,000. Upgrade your plan to raise it — new limits apply instantly and your usage counters reset."} ``` ## Job statuses [Section titled “Job statuses”](#job-statuses) | `status` | `terminal` | Meaning | | ------------- | ---------- | ---------------------------------------------------- | | `queued` | `false` | Waiting to start | | `processing` | `false` | Running | | `completed` | `true` | Finished; Unknown results refunded | | `failed` | `true` | Stopped with an error; unanswered addresses refunded | | `interrupted` | `true` | Stopped early; it may be resumed on our side | | `cancelled` | `true` | Cancelled | # API overview > What the EmailShield REST API does, who can use it, how to authenticate, and how much you can send. The EmailShield API lets your own tools do what the dashboard does: verify one address or a list, follow the job, and read the results. JSON in, JSON out. | | | | ------------------ | ---------------------------------------------------------------------------------------------- | | **Base URL** | `https://app.emailshield.co/api` | | **Authentication** | `X-API-Key: evf_…` header on every request | | **Format** | JSON request and response bodies | | **Who can use it** | Any paid account — any plan, or a single [top-up](/getting-started/credits-and-plans/#top-ups) | ## What you can do [Section titled “What you can do”](#what-you-can-do) | Endpoint | What it does | | ------------------------------------------------------------ | ---------------------------------------------- | | POST `/v1/verify/single` | Verify one address and wait for the answer | | POST `/v1/verify/bulk` | Submit up to 50,000 addresses as a job | | GET `/v1/jobs/{job_id}` | Job progress and totals — the endpoint to poll | | GET `/v1/jobs/{job_id}/results` | Results, page by page | | GET `/v1/jobs` | Your recent jobs | | GET `/v1/account`, `/v1/account/credits`, `/v1/subscription` | Plan and credit balance | | DELETE `/v1/keys/{key_id}` | Revoke an API key | Every endpoint, with request and response fields: [API reference](/api/reference/). ## API keys [Section titled “API keys”](#api-keys) Create keys in the app under **Profile → API**. A key starts with `evf_` and is shown **once** — copy it into your secret store straight away. To rotate a key, create a new one, update your integration, then revoke the old one. Keep keys on your server. Never put one in a browser, a mobile app or a public repository. ## Credits [Section titled “Credits”](#credits) The API uses the same credits as the dashboard: **1 credit per unique address**, charged when the job is submitted, with [Unknown results refunded](/getting-started/credits-and-plans/#how-the-refund-works) when it finishes. Duplicates within one call are removed before charging. Reading jobs and results is free. ## How much you can send [Section titled “How much you can send”](#how-much-you-can-send) API allowances count **verifications** (addresses), not requests, and they’re shared by every key on your account. Polling and reading results never count. | Plan | Keys | Per hour | Per day | | --------------------- | ---- | --------------- | --------- | | Starter | 1 | 10,000 | 10,000 | | Growth | 3 | No hourly limit | 50,000 | | Plus | 3 | No hourly limit | 100,000 | | Pro, Scale, Scale Max | 10 | Unlimited | Unlimited | Top-ups raise the allowance too, by the total you’ve ever bought: **any top-up** gives 1 key and 10,000 a day; **100,000+** gives 3 keys and 50,000 a day; **500,000+** is unlimited. If you have both a plan and top-ups, the more generous limit applies. One bulk call carries up to **50,000 addresses**. For bigger lists, split them into several calls, or upload the file in the dashboard (up to 2,000,000). The dashboard is never rate-limited. ## Getting results: poll or webhook [Section titled “Getting results: poll or webhook”](#getting-results-poll-or-webhook) Bulk jobs run in the background. Either: * **Poll** `GET /v1/jobs/{job_id}` every 10–30 seconds until `terminal` is `true`, then read the results; or * **Pass a `webhook_url`** when you submit, and we’ll `POST` a signed message to it when the job finishes. [Webhooks →](/api/webhooks/) Ready to try it? [API quickstart →](/api/quickstart/) # API quickstart > Verify your first addresses through the API in five minutes, with curl and Python. 1. **Create a key.** In the app, open **Profile → API** and create a key. Copy it — it’s shown only once. (No API tab? API access starts with any purchase.) 2. **Check it works.** ```bash export EMAILSHIELD_API_KEY="evf_your_api_key" curl https://app.emailshield.co/api/v1/account/credits \ -H "X-API-Key: $EMAILSHIELD_API_KEY" ``` ```json {"credits": 49000, "credits_used_today": 1000, "credits_used_this_month": 12000, "plan_limit": null, "renewal_date": "2026-11-08T10:00:00Z"} ``` 3. **Verify one address.** The answer comes back in the same response, usually within a few seconds (at most 30). ```bash curl -X POST https://app.emailshield.co/api/v1/verify/single \ -H "X-API-Key: $EMAILSHIELD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "jane.doe@example.com"}' ``` ```json { "email": "jane.doe@example.com", "status": "valid", "valid": true, "score": 0.95, "reason": "Mailbox confirmed", "details": {"primary_label": "valid", "tags": [], "label_reasons": ["Mailbox confirmed"]}, "credits_used": 1, "credits_remaining": 48999, "processed_at": "2026-10-09T12:00:03Z" } ``` 4. **Submit a list.** Up to 50,000 addresses per call. You get a `job_id` straight away. ```bash curl -X POST https://app.emailshield.co/api/v1/verify/bulk \ -H "X-API-Key: $EMAILSHIELD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "October campaign", "emails": ["jane.doe@example.com", "info@example.org"]}' ``` ```json {"job_id": "3f1c…", "status": "queued", "total_emails": 2, "credits_used": 2, "credits_remaining": 48997, "created_at": "2026-10-09T12:01:00Z"} ``` 5. **Wait for it to finish.** Poll the job until `terminal` is `true` (or use a [webhook](/api/webhooks/)). ```bash curl https://app.emailshield.co/api/v1/jobs/3f1c… -H "X-API-Key: $EMAILSHIELD_API_KEY" ``` 6. **Read the results**, 100 per page by default (up to 1,000), until `has_more` is `false`. ```bash curl "https://app.emailshield.co/api/v1/jobs/3f1c…/results?page=1&per_page=1000" \ -H "X-API-Key: $EMAILSHIELD_API_KEY" ``` ## The same in Python [Section titled “The same in Python”](#the-same-in-python) ```python import os, time, requests BASE = "https://app.emailshield.co/api" session = requests.Session() session.headers["X-API-Key"] = os.environ["EMAILSHIELD_API_KEY"] emails = ["jane.doe@example.com", "info@example.org"] job = session.post(f"{BASE}/v1/verify/bulk", json={"name": "October campaign", "emails": emails}).json() while True: status = session.get(f"{BASE}/v1/jobs/{job['job_id']}").json() if status["terminal"]: break time.sleep(15) page, rows = 1, [] while True: data = session.get(f"{BASE}/v1/jobs/{job['job_id']}/results", params={"page": page, "per_page": 1000}).json() rows += data["results"] if not data["has_more"]: break page += 1 for row in rows: print(row["email"], row["details"].get("primary_label")) ``` Read `details.primary_label`, not `status`, on job results In `/v1/jobs/{job_id}/results`, each row’s `status` is only `valid` or `invalid` (from the `valid` flag) — so a catch-all or unknown address also says `invalid` there. The real verdict is `details.primary_label` plus `details.tags`. See [Reading a result](/api/reference/#reading-a-result). ## Before you go live [Section titled “Before you go live”](#before-you-go-live) * Remove obviously broken addresses before sending: **one address that isn’t valid email syntax makes the whole bulk call fail** with `422`. * Handle `402` (out of credits), `429` (over your API allowance) and `503` (new jobs paused, retry after the `Retry-After` header). [Limits and errors →](/api/limits-and-errors/) # API reference > Every public EmailShield API endpoint — request fields, response fields and examples. Base URL `https://app.emailshield.co/api`. Every request needs the header `X-API-Key: evf_…`. Request bodies are JSON (`Content-Type: application/json`). ## Verify [Section titled “Verify”](#verify) ### POST `/v1/verify/single` [Section titled “POST /v1/verify/single”](#post-v1verifysingle) Verifies one address and returns the answer in the response. Waits up to 30 seconds; if there’s no answer by then you get `408` and the credit is refunded. Costs 1 credit. **Body** | Field | Type | | | ------------ | ------- | ------------------------------------------------- | | `email` | string | **Required.** The address to verify. | | `check_smtp` | boolean | Default `true`. Ask the mail server. Leave it on. | **Response `200`** | Field | Type | | | ------------------- | ------- | -------------------------------------------------------- | | `email` | string | | | `status` | string | `valid`, `invalid`, or `risky` (catch-all or disposable) | | `valid` | boolean | | | `score` | number | Confidence, 0 to 1 | | `reason` | string | Plain-English reason, e.g. `Mailbox confirmed` | | `details` | object | See [Reading a result](#reading-a-result) | | `credits_used` | integer | `1` | | `credits_remaining` | integer | Plan credits left (see note below) | | `processed_at` | string | ISO 8601 time | ### POST `/v1/verify/bulk` [Section titled “POST /v1/verify/bulk”](#post-v1verifybulk) Submits a list as a background job and returns `202` straight away. Costs 1 credit per unique address, charged now; Unknown results are refunded when the job finishes. **Body** | Field | Type | | | ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `emails` | string\[] | **Required.** 1 to 50,000 addresses. Duplicates (ignoring case) are removed before charging. Every item must be a syntactically valid address, or the whole call fails with `422`. | | `name` | string | Optional. The list name shown in the dashboard (up to 255 characters). | | `webhook_url` | string | Optional. HTTPS URL we `POST` to when the job finishes. See [Webhooks](/api/webhooks/). | | `priority` | string | `normal` (default), `high` or `urgent`. | | `check_smtp` | boolean | Default `true`. | **Response `202`** ```json { "job_id": "3f1c2a4e-…", "status": "queued", "total_emails": 2, "estimated_seconds": 0, "credits_used": 2, "credits_remaining": 48997, "created_at": "2026-10-09T12:01:00Z" } ``` `estimated_seconds` is a rough guess; don’t build timeouts on it. ## Jobs [Section titled “Jobs”](#jobs) ### GET `/v1/jobs/{job_id}` [Section titled “GET /v1/jobs/{job_id}”](#get-v1jobsjob_id) Job progress and totals. This is the cheap endpoint to poll. | Field | Type | | | ------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------- | | `job_id` | string | | | `status` | string | `queued`, `processing`, `completed`, `failed`, `interrupted` or `cancelled` | | `terminal` | boolean | `true` when the job has stopped. **Stop polling when this is `true`.** | | `total_emails` | integer | | | `processed` | integer | Addresses with a final answer so far | | `progress_percent` | number | | | `verdicts` | object | Counts by verdict: `valid`, `invalid`, `catch_all`, `risky`, `role_based`, `disposable`, `unknown` | | `buckets` | object | Counts by group: `safe_to_send`, `risky`, `invalid` | | `created_at`, `started_at`, `completed_at` | string | ISO 8601 times | | `error` | string \| null | Set when the job failed | `valid_count` and `invalid_count` are also returned for older integrations; use `verdicts` and `buckets` instead. ### GET `/v1/jobs/{job_id}/results` [Section titled “GET /v1/jobs/{job_id}/results”](#get-v1jobsjob_idresults) The answers, page by page. Rows appear while the job runs — only final answers are stored — so you can start reading before it finishes. | Query | | | | ---------- | ------- | ----------------------------- | | `page` | integer | Default `1` | | `per_page` | integer | Default `100`, maximum `1000` | ```json { "job_id": "3f1c2a4e-…", "status": "completed", "total": 2, "page": 1, "per_page": 100, "has_more": false, "results": [ { "email": "jane.doe@example.com", "status": "valid", "valid": true, "score": 0.95, "reason": "Mailbox confirmed", "details": {"primary_label": "valid", "tags": [], "label_reasons": ["Mailbox confirmed"]} }, { "email": "info@example.org", "status": "invalid", "valid": false, "score": 0.5, "reason": "Domain accepts all addresses", "details": { "primary_label": "catch_all", "tags": [{"label": "role", "confidence": 0.8}], "label_reasons": ["Domain accepts all addresses", "Role-based address"], "safe_to_send": "risky", "safe_to_send_score": 22 } } ] } ``` `status` on result rows is only `valid` or `invalid` It mirrors the `valid` flag, so catch-all, unknown and risky rows also read `invalid` here — like the second row above. Always decide from `details.primary_label` and `details.tags`. ### GET `/v1/jobs` [Section titled “GET /v1/jobs”](#get-v1jobs) Your recent jobs, newest first. | Query | | | | -------- | ------- | --------------------------------- | | `status` | string | Optional filter, e.g. `completed` | | `limit` | integer | Default `20`, maximum `100` | | `offset` | integer | Default `0` | Returns `{"jobs": [{"job_id", "status", "total_emails", "processed", "progress_percent", "created_at"}], "limit", "offset"}`. ## Reading a result [Section titled “Reading a result”](#reading-a-result) The verdict lives in `details`: | Field | | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- | | `primary_label` | `valid`, `invalid`, `catch_all`, `inbox_full`, `disabled`, `unknown` or `not_verified` | | `tags` | List of `{"label", "confidence"}` with `label` one of `spam_trap`, `disposable`, `role` | | `label_reasons` | Plain-English reasons, e.g. `Mailbox confirmed`, `Domain has no mail server` | | `safe_to_send`, `safe_to_send_score`, `safe_to_send_reasons` | Catch-all rows only: the [send-risk rating](/results/catch-all/#the-send-risk-rating) | `details` can carry other fields too; treat anything not listed here as informational and subject to change. To match the dashboard’s categories, apply tags first, then the label: | If… | Dashboard category | Group | | ---------------------------------------------- | ----------------------------------- | ------------ | | tags contain `spam_trap` | Spam Trap | Invalid | | tags contain `disposable` | Disposable | Invalid | | tags contain `role` | Role | Risky | | `primary_label` is `valid` | Valid | Safe to send | | `primary_label` is `catch_all` | Catch-All | Risky | | `primary_label` is `inbox_full` | Inbox Full | Risky | | `primary_label` is `invalid` or `disabled` | Invalid / Disabled / No Mail Server | Invalid | | `primary_label` is `unknown` or `not_verified` | Unknown / Not Verified (refunded) | Risky | ## Account [Section titled “Account”](#account) ### GET `/v1/account/credits` [Section titled “GET /v1/account/credits”](#get-v1accountcredits) `credits`, `credits_used_today` (last 24 hours), `credits_used_this_month` (last 30 days), `renewal_date`. ### GET `/v1/account` [Section titled “GET /v1/account”](#get-v1account) `plan`, `credits`, `credits_used_today` and a few account fields. ### GET `/v1/subscription` [Section titled “GET /v1/subscription”](#get-v1subscription) `status`, `plan`, `credits`, `renewal_date`, `has_billing`, `is_trial`, `trial_expired`. Note `credits` and `credits_remaining` in these responses count your plan credits. Top-up credits are spent after plan credits and may not be included in the number. ## Keys [Section titled “Keys”](#keys) ### DELETE `/v1/keys/{key_id}` [Section titled “DELETE /v1/keys/{key_id}”](#delete-v1keyskey_id) Revokes a key. Returns `204`, or `404` if there’s no such key. Keys are created in the app under **Profile → API**. # Webhooks > Get a signed POST when a bulk job finishes instead of polling, and verify the signature. Pass a `webhook_url` when you submit a bulk job and EmailShield will `POST` to it once, when the job finishes. You don’t have to poll. ```bash curl -X POST https://app.emailshield.co/api/v1/verify/bulk \ -H "X-API-Key: $EMAILSHIELD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"emails": ["jane.doe@example.com"], "webhook_url": "https://hooks.yourdomain.com/emailshield"}' ``` Webhooks are set per job, on `POST /v1/verify/bulk`. ## What we send [Section titled “What we send”](#what-we-send) ```http POST /emailshield HTTP/1.1 Host: hooks.yourdomain.com Content-Type: application/json User-Agent: EmailShield-Webhook/1 X-EmailShield-Event: job.completed X-EmailShield-Job: 3f1c2a4e-… X-EmailShield-Timestamp: 1791633663 X-EmailShield-Signature: v1=5d0c…e9 {"event":"job.completed","job_id":"3f1c2a4e-…","status":"completed","total_emails":2,"processed_emails":2,"verdicts":{"valid":1,"invalid":0,"catch_all":1,"risky":0,"role_based":0,"disposable":0,"unknown":0},"created_at":"2026-10-09T12:01:00Z","completed_at":"2026-10-09T12:03:41Z","results_url":"/v1/jobs/3f1c2a4e-…/results"} ``` | Field | | | ---------------------------------- | ------------------------------------------------------------------------------------ | | `event` | `job.completed`, or `job.failed` when the job failed or stopped early | | `job_id` | | | `status` | `completed` or `failed` | | `total_emails`, `processed_emails` | | | `verdicts` | Counts by verdict, as on [`GET /v1/jobs/{job_id}`](/api/reference/#get-v1jobsjob_id) | | `created_at`, `completed_at` | ISO 8601, UTC | | `results_url` | Path of the results endpoint. Prefix it with `https://app.emailshield.co/api` | | `error` | Only on `job.failed` | The message tells you the job is done; fetch the rows from `results_url` with your API key. ## Verify the signature [Section titled “Verify the signature”](#verify-the-signature) Every webhook is signed with your account’s **signing secret** (starts with `whsec_`). Find it, copy it or rotate it in the app under **Profile → API → Webhooks**. One secret covers every job on your account. Rotating takes effect immediately, so update your receiver at the same time. To check a webhook: 1. Take the **raw request body** exactly as received — don’t parse and re-serialise it. 2. Build the string `.`. 3. Compute HMAC-SHA256 of it with your secret, as lowercase hex. 4. Compare it with the part after `v1=` in `X-EmailShield-Signature`, using a constant-time comparison. 5. Reject the message if the timestamp is more than about **300 seconds** from now. ```python import hmac, hashlib, time def verify_emailshield(secret: str, raw_body: bytes, timestamp: str, signature: str) -> bool: if abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(f"v1={expected}", signature) ``` ```javascript import crypto from 'node:crypto'; export function verifyEmailShield(secret, rawBody, timestamp, signature) { if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex'); return expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } ``` ## Delivery and retries [Section titled “Delivery and retries”](#delivery-and-retries) * Reply with any **2xx** within **10 seconds** to acknowledge it. Do the slow work afterwards. * On a timeout or a non-2xx answer we retry: **5 attempts in total**, waiting 2 s, 10 s, 60 s and 300 s between them — about six minutes. Then we stop. * Redirects are not followed. * Each job’s webhook is delivered at most once successfully. Use `job_id` to ignore duplicates anyway. If you might miss a webhook (deploys, outages), keep a fallback: poll `GET /v1/jobs/{job_id}` for any job you haven’t heard about. ## URL rules [Section titled “URL rules”](#url-rules) The `webhook_url` must: * use **`https://`**; * resolve to a **public** address (no localhost, private networks or similar) — checked when you submit and again before every delivery; * use port 443 or a port of 1024 or above, with no username or password in the URL; * be at most 2,048 characters. A URL that breaks these rules is refused when you submit the job, with `422` and the reason. Nothing is charged. # EmailShield for AI agents > Plain-text docs for assistants and agents, and the rules an agent should follow before spending credits. These docs are written to be read by people **and** by AI assistants. If you’re building an agent that cleans lists, or asking an assistant to help you integrate, point it here. ## Plain-text versions [Section titled “Plain-text versions”](#plain-text-versions) | File | What’s in it | | ------------------------------------ | ---------------------------------------- | | [`/llms.txt`](/llms.txt) | Short index with the key facts and links | | [`/llms-small.txt`](/llms-small.txt) | The docs, compacted | | [`/llms-full.txt`](/llms-full.txt) | Every page in one file | ## Rules for an agent using the API [Section titled “Rules for an agent using the API”](#rules-for-an-agent-using-the-api) * **Every verification spends a credit.** Check `GET /v1/account/credits` before large jobs, and confirm with the person before spending a large share of their balance. * **Deduplicate and drop malformed addresses first.** One malformed address fails the whole bulk call with `422`. * **Batch.** One `POST /v1/verify/bulk` with up to 50,000 addresses, not thousands of single calls. * **Poll gently** (every 10–30 s) or use a [webhook](/api/webhooks/). Stop when `terminal` is `true`. * **Read `details.primary_label` and `details.tags`**, not the per-row `status`, to decide what’s safe. [Reading a result →](/api/reference/#reading-a-result) * **Only `valid` is safe to send.** Treat `catch_all`, `unknown`, `not_verified` and the `role` tag as risky; never mail `invalid`, `disabled`, `spam_trap` or `disposable`. * **Never guess a verdict** for an address that came back `unknown` — re-verify it later instead. * **Keep the API key secret.** Never print it, log it or put it in client-side code. * On `429` wait for `X-RateLimit-Reset`; on `503` wait for `Retry-After`. # Questions and answers > Short answers to the questions people ask most about EmailShield. ## Does EmailShield send an email to the people on my list? [Section titled “Does EmailShield send an email to the people on my list?”](#does-emailshield-send-an-email-to-the-people-on-my-list) No. It starts the conversation a real email would with the recipient’s mail server, asks whether the mailbox exists, and stops before anything is sent. [How verification works →](/results/how-verification-works/) ## Is a “valid” address guaranteed to reach the inbox? [Section titled “Is a “valid” address guaranteed to reach the inbox?”](#is-a-valid-address-guaranteed-to-reach-the-inbox) No one can promise that. *Valid* means the mailbox exists and accepts mail. Whether your email lands in the inbox or in spam depends on your domains, your inboxes and what you write. ## Why was I charged for invalid addresses? [Section titled “Why was I charged for invalid addresses?”](#why-was-i-charged-for-invalid-addresses) Because “this mailbox doesn’t exist” is a real answer — it’s the answer that stops a bounce. You only get credits back for results we couldn’t answer (Unknown, Not Verified). [Credits and plans →](/getting-started/credits-and-plans/) ## The same address was valid last month and invalid now. Which is right? [Section titled “The same address was valid last month and invalid now. Which is right?”](#the-same-address-was-valid-last-month-and-invalid-now-which-is-right) Probably both. People leave jobs and their mailboxes are closed all the time, which is why we recommend verifying the week you send. ## Can I verify the same list twice? [Section titled “Can I verify the same list twice?”](#can-i-verify-the-same-list-twice) Yes. Within 48 hours you’ll get the same answers back. After that, it’s checked fresh. ## Why am I asked for a company and phone number? [Section titled “Why am I asked for a company and phone number?”](#why-am-i-asked-for-a-company-and-phone-number) Trial accounts fill in a short profile before verifying. It keeps free credits away from throwaway accounts and lets us help you get set up. Paid accounts aren’t asked. ## I paid — why am I asked for business details? [Section titled “I paid — why am I asked for business details?”](#i-paid--why-am-i-asked-for-business-details) After your first payment we ask once for the business details that go on your invoices. You won’t be asked again. ## New jobs say “Technical maintenance”. What’s going on? [Section titled “New jobs say “Technical maintenance”. What’s going on?”](#new-jobs-say-technical-maintenance-whats-going-on) New jobs are occasionally paused for a short time. Running jobs keep going, and nothing is charged for a job that wasn’t accepted. Try again later or ask in the chat. ## How long do you keep my data? [Section titled “How long do you keep my data?”](#how-long-do-you-keep-my-data) Your uploaded columns (names, companies and so on) are kept for 30 days after a job finishes so you can export them. Verification results stay until you delete the job, which you can do at any time from the results list. ## Do credits expire? [Section titled “Do credits expire?”](#do-credits-expire) Top-up credits never expire. If a plan or trial ends you keep 1,000 plan credits plus all your top-up credits; the rest are held and come back when you subscribe again. ## Can I get API access on the trial? [Section titled “Can I get API access on the trial?”](#can-i-get-api-access-on-the-trial) API access starts with any purchase — any plan or a single top-up. [API overview →](/api/overview/) # Get help > Chat with the team in the app or here in the docs, join the EmailShield Slack, or message us on Telegram. Stuck, or not sure what a result means? A real person on the team will help. Pick whichever channel suits you. ## Chat with us [Section titled “Chat with us”](#chat-with-us) The chat bubble in the bottom-right corner of every page — here in the docs and inside the [EmailShield app](https://app.emailshield.co) — goes straight to the support team. In the app you’re signed in already, so we can see the job you’re asking about; here in the docs the chat asks you to sign in first. [Chat with us](https://slack.emailshield.co) ## Slack [Section titled “Slack”](#slack) Join the community Slack at **[slack.emailshield.co](https://slack.emailshield.co)**. Ask questions, share what’s working for other cold emailers, and hear about new features first. It’s the same Slack used by Maildeck and LeadSonar customers, so you can ask about your whole stack in one place. ## Telegram [Section titled “Telegram”](#telegram) Message **[@ScalingRoomSupportBot](https://t.me/ScalingRoomSupportBot)** on Telegram. ## What to include [Section titled “What to include”](#what-to-include) You’ll get an answer faster if you tell us: * **The job** — its name, or the link from your browser’s address bar when you have the results open. * **What you expected** and **what you saw** — for example “this address is marked invalid but I emailed them last week”. * **A few example addresses**, if it’s about specific results. Never send your API key We will never ask for your API key or password. If you’ve pasted a key somewhere public, revoke it under **Profile → API** in the app and create a new one. # Glossary > The words you'll see in EmailShield and in cold email, in plain English. **Accept-all** — see *Catch-all*. **Blacklist / blocklist** — a public list of senders (IP addresses or domains) that many mail servers refuse or filter. Spamhaus is the best known. **Bounce** — an email that comes back undelivered. A *hard* bounce means the address doesn’t exist; a *soft* bounce is temporary, like a full inbox. **Catch-all** — a domain that accepts mail for every address, real or not, so a single mailbox can’t be confirmed. [More →](/results/catch-all/) **Credit** — what you spend in EmailShield. One credit checks one address. [More →](/getting-started/credits-and-plans/) **DKIM** — a signature on outgoing email that proves it came from your domain and wasn’t changed on the way. **DMARC** — a DNS record that tells receivers what to do with mail that fails SPF and DKIM. **Disposable address** — a temporary inbox from a throwaway email service. **Greylisting** — a mail server deliberately answering “try again later” to unfamiliar senders. **ICP** — ideal customer profile: a short description of the companies and people you sell to. **MX record** — the DNS record that says which mail server receives email for a domain. No MX record, no mail. **Role address** — a shared inbox such as `info@`, `sales@` or `support@` rather than a person. **Sender reputation** — how much mailbox providers like Google and Microsoft trust your domain and inboxes. Bounces and spam complaints lower it. **Sequencer** — the tool that sends your cold emails and follow-ups on a schedule (Instantly, PlusVibe, Smartlead…). **SPF** — a DNS record listing the servers allowed to send email for your domain. **Spam trap** — an address that exists only to catch senders who don’t clean their lists. Mailing one can get you blacklisted. **Verification** — checking that an email address exists and accepts mail, without sending an email to it. **Warm-up** — gradually increasing how much a new inbox sends so mailbox providers learn to trust it. # Catch-all addresses > Why some domains accept every address, how EmailShield detects them, and how to decide which catch-all addresses to send to. A **catch-all** (or *accept-all*) domain tells every sender “yes, that mailbox exists” — for every address, real or not. Some companies set it up so they never lose mail sent to a typo; many security gateways (Proofpoint, Mimecast and similar) behave the same way in front of the real mailbox. For you, it means **the mail server’s “yes” proves nothing**. `jane@acme.com` and `zzz-made-up@acme.com` get the same answer. ## How EmailShield detects it [Section titled “How EmailShield detects it”](#how-emailshield-detects-it) Catch-all is a property of the domain, so it’s tested once per domain, not once per address. EmailShield asks the domain’s mail server about an address that is realistic but made up. If the server says that one exists too, the domain is catch-all. While that test runs, addresses on the domain show as **Still resolving**. They become **Valid** (the domain isn’t catch-all, so its “yes” is real) or **Catch-All** before the job finishes. ## Getting past the gateway [Section titled “Getting past the gateway”](#getting-past-the-gateway) When a catch-all answer comes from a security gateway, EmailShield tries to ask the email platform behind it. If that platform confirms the mailbox, the address is upgraded to **Valid** with the reason *Mailbox confirmed at the mail platform behind the gateway*. That check can only ever move an address up — a “no” from behind the gateway never makes an address invalid. ## The send-risk rating [Section titled “The send-risk rating”](#the-send-risk-rating) Every catch-all address also gets a rating, shown in the results and in the export’s `safe_to_send_tier` column (with a 0–100 `safe_to_send_score`): | Rating | What it means | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **likely** | The address follows the same pattern as the domain’s other addresses (for example `first.last@`) and reads like a real person’s name. | | **uncertain** | Some signals point each way. | | **risky** | Doesn’t match the domain’s pattern, looks generated, or looks like a spam trap. | The rating is worked out from your list itself — no third-party data is involved — and it’s a judgement, not a confirmation. It never changes the category: a *likely* catch-all is still a catch-all. ## Should you send to catch-alls? [Section titled “Should you send to catch-alls?”](#should-you-send-to-catch-alls) It depends on how much risk your domains can take. Common approaches: 1. **Leave them out.** The safest choice, especially for new domains. 2. **Send only to *likely*.** Turn on **Count good catch-all addresses as safe to send** under **Views & Export**, and the *likely* ones join your Safe to send export. 3. **Send to them separately.** Put catch-alls in their own campaign, at a lower daily volume, and pause it if bounces rise. Caution Catch-all addresses are charged: “this domain accepts everything” is a real answer, and it’s the one that stops you assuming a list is cleaner than it is. # How verification works > What happens to every address between upload and verdict, and why no email is ever sent. Every address goes through the same steps. The cheap, certain checks come first, so a mail server is only asked when it has to be. ## 1. Basic checks [Section titled “1. Basic checks”](#1-basic-checks) For every address: * **Syntax** — is it a well-formed email address? * **Disposable domain** — is it a throwaway inbox provider? * **Spam traps** — does it look like a known or likely trap? These are never contacted. * **Administrative addresses** — `abuse@`, `postmaster@` and similar are never contacted. * **Mail server (MX) lookup** — does the domain receive mail at all? A domain with no mail server, or one that says it takes no mail, makes every address on it **No Mail Server**. * **Parked domains** — domains parked at a domain seller aren’t checked further. ## 2. Answers we already have [Section titled “2. Answers we already have”](#2-answers-we-already-have) * If a mailbox told us **“does not exist”** in the last 45 days, we reuse that answer instead of asking again. * A definite answer for the same address is reused for 48 hours, so the same list uploaded twice in a row gets the same results. ## 3. Asking the mail server — without sending an email [Section titled “3. Asking the mail server — without sending an email”](#3-asking-the-mail-server--without-sending-an-email) For everything left, EmailShield connects to the recipient’s mail server and starts the conversation a real email would — *hello, this is who the mail is from, this is who it’s for* — and stops there. The server answers whether it would accept mail for that mailbox. **No message is ever sent**, and the person never sees anything. This is done from dedicated verification infrastructure, at a steady, polite pace for each receiving provider, so servers keep giving honest answers. ## 4. Catch-all test [Section titled “4. Catch-all test”](#4-catch-all-test) Once per domain, a realistic made-up address is tested as well. If the server accepts that too, the domain accepts everything and its answers can’t confirm a mailbox. [More about catch-alls →](/results/catch-all/) ## 5. Microsoft 365 [Section titled “5. Microsoft 365”](#5-microsoft-365) For domains hosted on Microsoft 365, EmailShield asks Microsoft’s directory. A positive answer becomes **Valid (MS365)**. A negative isn’t trusted — the directory leaves out aliases and shared mailboxes — so it’s followed up with a mail-server check instead of being called invalid. ## 6. Retries [Section titled “6. Retries”](#6-retries) “Try again later” answers are retried after a pause; timeouts are retried by a different route with longer waits. A definite “this mailbox doesn’t exist” is final straight away. After a few attempts without an answer, the address is **Unknown** and refunded. ## What EmailShield doesn’t do [Section titled “What EmailShield doesn’t do”](#what-emailshield-doesnt-do) * It **never sends an email** to the addresses on your list. * It **never guesses**. An address without a real answer is Unknown, not valid. * It **doesn’t promise deliverability.** A valid mailbox can still filter your email to spam; that depends on your domains, your inboxes and what you write. See [Maildeck’s deliverability guides](https://docs.maildeck.co/). # Reading your results > Safe to send, risky and invalid — what each group means and what to do with it. Every job’s results page opens on **Overview**, which answers one question: *who do I mail?* Every address lands in one of three groups. | Group | What it means | What to do | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Safe to send | The receiving mail server, or Microsoft’s own directory, confirmed this mailbox exists. | **Mail these.** | | Risky | Accepted, but nothing could confirm the individual mailbox — accept-all (catch-all) domains, shared role inboxes, full inboxes and addresses that never gave an answer. | **Your call.** Leave them out, or send to them separately at a lower volume. | | Invalid | No mailbox at all, or a real one that would cost you reputation (spam trap, disposable). | **Don’t mail.** | ## What’s in each group [Section titled “What’s in each group”](#whats-in-each-group) | Group | Categories | | ------------ | ------------------------------------------------------------------- | | Safe to send | Valid, Valid (MS365) | | Risky | Catch-All, Role, Inbox Full, Unknown, Not Verified, Still Resolving | | Invalid | Invalid, Disabled, No Mail Server, Spam Trap, Disposable | The **Breakdown** tab shows each category on its own. [Every status explained →](/results/statuses/) ## A healthy result [Section titled “A healthy result”](#a-healthy-result) There’s no “normal” result: it depends entirely on where the list came from. As a rough guide: * **A fresh list from a good source** is mostly safe to send, with a few percent invalid. * **A lot of risky** usually means a lot of catch-all domains (common with companies that use security gateways) — see [Catch-all addresses](/results/catch-all/). * **A lot of invalid** means the list is old or the source is poor. Check where it came from before you buy more from the same place. ## Changing how groups are counted [Section titled “Changing how groups are counted”](#changing-how-groups-are-counted) Two options under **Views & Export** move addresses between groups, on the page and in the export: * **Count good catch-all addresses as safe to send** — catch-alls that EmailShield rates *likely* move into Safe to send. * **Treat spam traps and disposable addresses as risky** instead of invalid. Both are off by default. They change only how results are grouped, never the verdicts themselves. ## While a job is still running [Section titled “While a job is still running”](#while-a-job-is-still-running) Addresses waiting on their domain’s catch-all check show as **Still resolving**. They aren’t a verdict yet: they become Valid or Catch-All before the job finishes, and the page’s percentages only count settled answers. # Every status explained > Every category EmailShield can return, what it means, which group it's in and whether it's charged. The **Breakdown** tab and the Breakdown export use these categories. If an address matches more than one — say, a role address on a disposable domain — the most important one wins: **spam trap**, then **disposable**, then **role**, then the mailbox answer. ## Confirmed [Section titled “Confirmed”](#confirmed) | Category | What it means | Group | Charged | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------- | | **Valid** | The mail server confirmed the mailbox exists. | Safe to send | Yes | | **Valid (MS365)** | A Microsoft 365 / Outlook address confirmed by Microsoft’s own directory rather than by the mail server. Written `valid` in every export. A very small share may be accounts without an active mailbox. | Safe to send | Yes | ## Risky [Section titled “Risky”](#risky) | Category | What it means | Group | Charged | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------- | | **Catch-All** | The domain accepts mail for every address, so this mailbox can’t be confirmed either way. Each one gets a *likely / uncertain / risky* rating — see [Catch-all addresses](/results/catch-all/). | Risky | Yes | | **Role** | A shared inbox such as `info@`, `sales@` or `support@`. It may exist, but shared inboxes rarely reply to cold email and some are watched closely. | Risky | Yes | | **Inbox Full** | The mailbox exists but is over its storage limit, so mail would bounce for now. Often a sign of an abandoned inbox. | Risky | Yes | ## Invalid [Section titled “Invalid”](#invalid) | Category | What it means | Group | Charged | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------- | | **Invalid** | The mail server said the mailbox does not exist, or the address isn’t valid email syntax (syntax errors are free). | Invalid | Yes | | **Disabled** | The account exists but is disabled or suspended. | Invalid | Yes | | **No Mail Server** | The domain has no mail server at all — no MX record, or one that explicitly says “this domain takes no mail”. A verdict about the whole domain. | Invalid | Yes | | **Spam Trap** | An address that looks like a known or likely spam trap. Never mailed, never checked. | Invalid | Yes | | **Disposable** | A throwaway inbox from a temporary-email provider. | Invalid | Yes | ## No answer — refunded [Section titled “No answer — refunded”](#no-answer--refunded) | Category | What it means | Group | Charged | | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------------- | | **Unknown** | We asked, but couldn’t get a definite answer. [Why this happens →](/results/unknowns/) | Risky | **Refunded** | | **Not Verified** | We never got to check this address. That’s a gap on our side, not a verdict. Re-run the list to get an answer. | Risky | **Refunded** | | **Still Resolving** | The job is still checking whether the domain is catch-all. These turn into Valid or Catch-All before the job finishes. | Risky | **Refunded** if still there at the end | ## Reasons [Section titled “Reasons”](#reasons) Every row also carries a short **reason** in the export, for example: | Reason | Usually means | | ------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Mailbox confirmed | Valid | | Mailbox does not exist | Invalid | | Domain accepts all addresses | Catch-All | | Mailbox confirmed at the mail platform behind the gateway | Valid — confirmed past a security gateway | | Server accepted but could not be confirmed | Unknown — see [Why some results are unknown](/results/unknowns/) | | Mailbox full | Inbox Full | | Account disabled or suspended | Disabled | | Role-based address | Role | | Administrative role address | Unknown — `abuse@`, `postmaster@` and similar are never checked | | Disposable email provider | Disposable | | Known spam trap / Possible spam trap | Spam Trap | | Domain has no mail server / Domain does not accept email | No Mail Server | | Domain is parked | Unknown — the domain is parked with a domain seller | | Server temporarily unavailable / Server response inconclusive | Unknown | | Mailbox could not be confirmed | Unknown — Microsoft 365 had no record, which is not proof the mailbox is missing | | Invalid email format | Invalid (free) | # Why some results are unknown > The reasons a mail server sometimes won't give a straight answer — and why unknown results are refunded. **Unknown means we asked and didn’t get a definite answer.** We never guess: an address we can’t confirm either way stays Unknown rather than being called valid or invalid. And because it isn’t an answer, **you don’t pay for it** — unknown results are refunded automatically when the job finishes. ## Common reasons [Section titled “Common reasons”](#common-reasons) * **The server answered “try again later”** and kept saying it. Some servers do this on purpose to slow down unknown senders (*greylisting*). We wait and retry, but only a few times. * **The server didn’t respond in time**, even after retries with longer timeouts. * **The server refused to answer us** — for example a policy that only accepts mail from known senders. That’s about the server, not the mailbox. * **The server said yes, but the domain’s catch-all test couldn’t be confirmed.** A “yes” only counts if we know the domain doesn’t say yes to everything, so these stay Unknown (*Server accepted but could not be confirmed*). * **Microsoft 365 had no record.** Microsoft’s directory doesn’t list aliases, shared mailboxes or groups, so “no record” there isn’t proof the mailbox is missing. We follow up with a mail-server check; if that can’t settle it, it stays Unknown. * **The domain’s DNS didn’t answer.** A failed lookup is never treated as “no mail server”. * **The domain is parked** with a domain seller. * **We deliberately didn’t check it** — administrative addresses like `abuse@` and `postmaster@` are never probed. **Not Verified** is different: it means we never got to check the address at all. That’s on us, and it’s refunded too. ## What to do with unknowns [Section titled “What to do with unknowns”](#what-to-do-with-unknowns) * **Run them again later.** Export the Unknown category from the Breakdown tab and upload it as a new list in a day or two. Many servers that were busy or cautious will answer next time. * **Treat them as risky** if you need to send now: in a separate, lower-volume campaign, or not at all. If a large share of a list comes back Unknown, tell us in the chat — we look into it. # Find leads with LeadSonar > LeadSonar finds the people; EmailShield checks their addresses. How the two work together. ![LeadSonar logo](/partners/leadsonar.svg) LeadSonar B2B lead finder — our sister product · [leadsonar.io](https://leadsonar.io) **LeadSonar is part 3 of your stack: it decides who your emails go to.** You describe the companies and people you sell to, and it finds them along with their work email addresses. [Part 1 Infrastructure ![](/partners/maildeck.png) Maildeck Domains and inboxes you send from ](/stack/maildeck/)[Part 2 Sequencer ![](/partners/instantly.png)![](/partners/plusvibe.png) Instantly, PlusVibe… Sends emails and follow-ups ](/stack/sequencer/)[Part 3 Leads ![](/partners/leadsonar.svg) LeadSonar The right people and their emails ](/stack/leadsonar/)[Part 4That's us Verification ![](/favicon.png) EmailShield Removes bad addresses first](/verify/upload-a-list/) ## EmailShield is built into LeadSonar [Section titled “EmailShield is built into LeadSonar”](#emailshield-is-built-into-leadsonar) You don’t have to leave LeadSonar to verify the leads you found there. LeadSonar runs EmailShield for you: 1. In **Lead Finder → My Leads**, select the leads. 2. Click **Enrich selected** and tick **EmailShield verification**. 3. Each lead gets an **email status**, so you know which ones are safe to send to. LeadSonar’s own guide walks through it step by step, including what it costs in LeadSonar credits: **[Verify with EmailShield — LeadSonar Docs](https://docs.leadsonar.io/before-you-send/verify-with-emailshield/)**. ## When to use EmailShield directly [Section titled “When to use EmailShield directly”](#when-to-use-emailshield-directly) Use the EmailShield app at [app.emailshield.co](https://app.emailshield.co) when: * **The list came from somewhere else** — another data provider, a CRM export, an event, a scrape. * **The list is large.** Upload a CSV or Excel file of any size your plan’s credits cover; see [Upload a list](/verify/upload-a-list/). * **You want the full breakdown** — every status, catch-all risk scoring and filtered exports, described in [Reading your results](/results/reading-your-results/). * **You’re building it into your own tools** with the [EmailShield API](/api/overview/). Verify the week you send A LeadSonar list is fresh when you export it, but people change jobs every month. If a list has sat for a few weeks, run it through EmailShield again before the campaign starts. ## Next [Section titled “Next”](#next) * [Load your clean list into your sequencer](/stack/sequencer/) * [LeadSonar Docs](https://docs.leadsonar.io/) — everything about finding leads. # Send from Maildeck > Maildeck gives your cold email somewhere safe to send from. Why verified lists and good infrastructure go together. ![Maildeck logo](/partners/maildeck.png) Maildeck Cold email infrastructure, set up for you — our sister product · [maildeck.co](https://maildeck.co) **Maildeck is part 1 of your stack: the domains and inboxes you send from.** It buys the domains, sets up the inboxes and their DNS records (SPF, DKIM and DMARC), and warms them up so they’re ready to send. ## Why it matters for verification [Section titled “Why it matters for verification”](#why-it-matters-for-verification) Cold email should never go out from your company’s main domain. You send from separate domains, so that if one gets a bad reputation your normal business email is untouched. Those domains take weeks of warm-up, and bounces are the quickest way to undo that work. That’s the deal between the two products: **Maildeck builds the reputation, EmailShield protects it.** Verify every list before it reaches a Maildeck inbox. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Maildeck Docs](https://docs.maildeck.co/) — domains, DNS, warm-up and safe sending limits. * [Maildeck’s guide to EmailShield](https://docs.maildeck.co/leads-verification/emailshield/) — the same story from their side. * [Check a domain’s setup with Full Scan](/tools/full-scan/) — see whether a sending domain’s SPF, DKIM and DMARC are in order. # Load into your sequencer > Take your verified list from EmailShield into Instantly, PlusVibe, Smartlead or any other sequencer. A **sequencer** (Instantly, PlusVibe, Smartlead, EmailBison…) sends your emails and follow-ups on a schedule across your inboxes. It’s part 2 of your stack, and it’s where your clean list ends up. ## From EmailShield to your sequencer [Section titled “From EmailShield to your sequencer”](#from-emailshield-to-your-sequencer) 1. **Verify the list** in EmailShield ([Upload a list](/verify/upload-a-list/)). 2. **Export the addresses you want to send to.** The simplest choice is the **Safe to send** group; see [Export your results](/verify/export/). 3. **Import the CSV** into your sequencer as a new lead list. Your original columns (first name, company, and so on) come back in the export, so your personalisation fields still work. 4. **Start the campaign** and keep an eye on the bounce rate for the first few days. What about risky addresses? *Risky* mostly means **catch-all**: the domain accepts every address, so the mailbox can’t be confirmed. Many teams send to these from a separate campaign at a lower volume, or only to the ones EmailShield marks *likely*. See [Catch-all addresses](/results/catch-all/). ## Keep lists fresh [Section titled “Keep lists fresh”](#keep-lists-fresh) A verified list doesn’t stay verified forever. If a campaign starts more than a couple of weeks after you verified, or the bounce rate climbs above about 2–5%, pause and run the remaining addresses through EmailShield again. # Full Scan > A free, one-off check of a sending domain, an address or an IP — blacklists, DNS, SSL, SPF, DKIM and DMARC in one pass. **Full Scan** is the quick health check for the things you send *from*. Before a campaign, point it at a sending domain and it tells you whether that domain is set up properly and whether anything is blocking it. Scanning one target is **free**. Open **Full Scan** in the app’s sidebar and enter a domain (or a website URL), an email address, or an IP address. ## What it checks [Section titled “What it checks”](#what-it-checks) **For a domain:** | Check | What it tells you | | ------------------- | ------------------------------------------------------------------------------------------- | | **MX records** | Whether the domain can receive mail (replies have to land somewhere). | | **SPF** | Whether the domain lists who’s allowed to send for it. | | **DKIM** | Whether outgoing mail is signed, so receivers can check it wasn’t altered. | | **DMARC** | Whether the domain has a policy telling receivers what to do with mail that fails SPF/DKIM. | | **SSL certificate** | Whether the website’s certificate is valid, and how many days until it expires. | | **Blacklists** | Whether the domain or its address appears on public blocklists. | **For an email address:** syntax, plus whether it’s disposable, a role address or a free provider (Gmail, Outlook…). It doesn’t check the mailbox itself — use a [verification](/verify/upload-a-list/) for that. **For an IP address:** blocklist checks. The result lists what’s wrong in order of severity, gives an overall score and suggests what to fix. A check that couldn’t answer isn’t a pass If a blocklist or DNS lookup didn’t respond, Full Scan says it was **unable to verify** that check instead of showing it as clean, and it doesn’t count against your score. ## Scanning many domains [Section titled “Scanning many domains”](#scanning-many-domains) You can also upload a list of targets for a bulk scan. Bulk scans cost credits per target: | Scan | Credits per target | | ---------------------- | ------------------ | | Full Scan (all checks) | 5 | | Blacklist check | 2 | | DNS lookup | 1 | | SSL check | 1 | ## When to run it [Section titled “When to run it”](#when-to-run-it) * When you set up **new sending domains**, before warm-up starts. * When **bounces or spam placement suddenly get worse**. * Every few weeks on the domains you send from most. Your domains and inboxes come from somewhere — if that’s Maildeck, see [Maildeck’s SPF, DKIM and DMARC guide](https://docs.maildeck.co/domains-dns/spf-dkim-dmarc/). # Export your results > Download the addresses you want to send to, with your original columns, in simple or detailed form. Every finished job can be downloaded as a CSV from its results page. There are two kinds of export, matching the two tabs on the page. ## Overview export — “who do I mail?” [Section titled “Overview export — “who do I mail?””](#overview-export--who-do-i-mail) The **Overview** tab groups every address into three: * Safe to send — the mailbox was confirmed. Mail these. * Risky — accepted, but the individual mailbox couldn’t be confirmed. Your call. * Invalid — don’t mail. Its export has a `status` column with exactly those three values, so the file says the same thing as the page. Pick the groups you want before you download — most people export only **Safe to send**. * **Export** — the result columns: email, status, score, tier (for catch-alls) and a plain-English reason. * **Email List Only** — just the email plus your original upload columns, ready to import into a sequencer. ## Breakdown export — every detail [Section titled “Breakdown export — every detail”](#breakdown-export--every-detail) The **Breakdown** tab shows every category separately (valid, catch-all, role, disposable, invalid, no mail server…). Its export’s `status` column is that precise category, and you can choose exactly which categories to include. [What each category means →](/results/statuses/) *Valid (MS365)* addresses are written as `valid` in every export. The **Full Report** version includes every field we have: | Column | What it means | | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `status` | The category (Breakdown) or the group (Overview) | | `confidence_score` | How sure we are, 0 to 1 | | `is_safe_to_send` | `true` for valid addresses | | `safe_to_send_tier` | For catch-all addresses only: `likely`, `uncertain` or `risky` — see [Catch-all addresses](/results/catch-all/) | | `safe_to_send_score` | The 0–100 score behind that tier | | `is_disposable`, `is_role_based`, `is_spamtrap`, `is_catch_all`, `is_disabled`, `is_free_email` | Individual flags | | `mx_records`, `mx_accepts_mail` | The domain’s mail servers | | `reason` | A plain-English reason, such as *Mailbox confirmed* or *Mailbox does not exist* | ## Your settings [Section titled “Your settings”](#your-settings) Under **Views & Export** on the results page you can: * **Count good catch-all addresses as safe to send** — moves catch-alls rated *likely* into Safe to send, on the chart and in the export. Off by default. * **Treat spam traps and disposable addresses as risky** instead of invalid. * Choose **simple or full columns** for the Overview export. These settings are remembered in your browser. Download your original columns within 30 days Your uploaded columns (names, companies…) are stored for 30 days after a job finishes, so export them in that window. The verification results themselves stay until you delete the job. ## Downloading a job that’s still running [Section titled “Downloading a job that’s still running”](#downloading-a-job-thats-still-running) You can download a job before it finishes. The file contains the answers so far; addresses still being checked are left out unless you ask for them. # Upload a list > File types, limits, how EmailShield finds the email column, and what happens to duplicates. Uploading a file is the main way to verify a list. Open the **Dashboard** in [the app](https://app.emailshield.co) and pick the **Upload** tab. ## What you can upload [Section titled “What you can upload”](#what-you-can-upload) | | | | ---------------------- | ------------------------------------------------------------- | | **File types** | `.csv` or `.txt` (export Excel or Google Sheets as CSV first) | | **Addresses per list** | Up to **2,000,000** | | **File size** | Up to **200 MB** | | **Separators** | Comma, semicolon, tab or pipe — detected automatically | A sample file is available on the Upload tab if you want to see the expected shape. ## How the email column is found [Section titled “How the email column is found”](#how-the-email-column-is-found) You don’t have to rename anything. EmailShield looks for a header such as **Email**, **Emails**, **Email Address**, **E-mail**, **Work Email**, **Personal Email**, **Email ID**, **Mail**, or any header ending in “email” or “mail”. Columns *about* email, like *Email Status*, are ignored. If no header matches, it picks the column that holds the most addresses in the first 50 rows. One email column only If two email-named columns both contain addresses, the upload stops with *“multiple headers w name email detected, leave only one”*. Delete or rename the column you don’t want checked, then upload again. In a `.txt` file, put one address per line. A first line without an `@` is treated as a header and skipped. ## What happens before checking starts [Section titled “What happens before checking starts”](#what-happens-before-checking-starts) * **Duplicates are removed**, ignoring capital letters (`Jane@Acme.com` and `jane@acme.com` count once). Duplicates are free. * **Addresses that aren’t valid email syntax** are marked *Invalid — Invalid email format* and never charged. * **Your other columns are kept** and put back next to each address when you export. * You’re charged **1 credit per remaining address**, up front. [Unknown results are refunded](/getting-started/credits-and-plans/#how-the-refund-works) when the job finishes. ## While the job runs [Section titled “While the job runs”](#while-the-job-runs) You’ll be taken to the job’s results page. It fills in as answers arrive, and you can leave at any time — the job keeps running on our side. Mail servers are asked at a steady, polite pace so they keep answering honestly, so a large list can take a while. To get an email when a job finishes, turn on **Job completion** under **Profile → Notifications**. Seeing “Technical maintenance”? Very occasionally new jobs are paused for maintenance. Jobs that are already running carry on; try again a bit later, or ask us in the chat. ## Other ways in [Section titled “Other ways in”](#other-ways-in) * **Paste** — paste addresses separated by new lines, commas or semicolons. Good for a few hundred. * **Single** — check one address. It runs as a one-address job and opens its result. * **API** — up to 50,000 addresses per call. See the [API quickstart](/api/quickstart/). * **Inside LeadSonar** — verify leads without leaving LeadSonar. See [Find leads with LeadSonar](/stack/leadsonar/).