# Email list cleaning checklist before a campaign

Ten steps in order: normalize and dedupe, import known bounces, verify in bulk, segment by verdict, authenticate, warm up, then keep it clean with monitors.

Date: 2026-09-03

Cleaning a list means removing addresses that will bounce, segmenting the ones that carry risk, and setting up the feedback that keeps the list clean after the send. Done in the right order it takes one bulk verification run and a few filters, and the order matters because every earlier step makes the later ones cheaper: duplicates and known bounces cost nothing to verify, so remove them first.

## 1. Normalize and dedupe

Lowercase every address, strip plus-tags, fold the dots out of Gmail usernames, and unwrap the forms people paste: display names, `mailto:` links, quotes, trailing punctuation. `Mia.K+promo@gmail.com` and `miak@gmail.com` are the same inbox. Spaw does this on every lookup and returns the result as `normalized_email`; bulk uploads count repeated normalized addresses as `duplicate_count` and verify them once.

## 2. Import what you already know is bad

If your sending platform has a bounce or unsubscribe export, load the hard bounces into your suppression list before verifying anything. Imports are free, take up to 10,000 addresses per request, and a suppressed address is answered without any work in every later batch, bulk and monitor run.

```bash
curl https://spaw.co/api/v1/email/suppressions \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["old@acme.com", "gone@example.org"]}'
```

## 3. Verify the whole list in one bulk job

Upload the file as it is. A bulk job keeps your columns, appends the verdict columns, and answers up to 100,000 addresses per job with a signed webhook when it finishes. Undeliverable answers, duplicates and suppressed addresses are free; you pay one credit per deliverable or risky answer.

```bash
curl https://spaw.co/api/v1/email/bulk \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["mia@acme.com", "info@example.org"], "webhook_url": "https://www.example.com/hooks/spaw"}'
```

```json
{
  "success": true,
  "data": {
    "job": {
      "id": 512,
      "status": "queued",
      "total": 2,
      "duplicate_count": 0,
      "webhook_secret": "8fJ2…40 characters…Qk1",
      "// 10 more fields": "see the endpoint reference"
    }
  }
}
```

Download the results as one CSV, or with `?variant=deliverable`, `risky` or `undeliverable` to get each segment as its own file.

## 4. Segment by verdict and reason

Three verdicts, and inside `risky` the reason decides the action.

| Verdict and reason | Action |
| --- | --- |
| `undeliverable`, any reason | Remove. Already on your suppression list. |
| `risky` · `disposable` | Remove from marketing sends. |
| `risky` · `likely_typo` | Fix with `did_you_mean` only if you can confirm with the contact; otherwise remove. |
| `risky` · `role` | Keep for B2B and transactional mail; exclude from promotional sequences. |
| `risky` · `catch_all` | Send in moderation; branch on `mailbox_confidence`. |
| `risky` · `mailbox_full` | Hold, retry in a few days. |
| `risky` · `implicit_mx` | Treat as probably dead unless the handshake confirmed the mailbox. |
| `deliverable`, `smtp_checked: false` | Send, but expect a few bounces; a `callback_url` re-check settles many of these for free. |
| `deliverable`, `mailbox_exists: true` | Send. |

The `risk_score` gives the same information as one number for tools that need a single threshold: 60 and above is high, 30 to 59 medium. Because the score is a published sum of weights, you can recompute it from the booleans in the CSV if your policy differs.

## 5. Decide the disposable and role policy per list

Marketing lists lose both. B2B lead lists keep role addresses and lose burners. Transactional recipients keep everything that is not undeliverable, because the message must be sent to the address the customer chose. Write the policy down; it is the difference between a clean list and a list that gets cleaned differently every quarter.

## 6. Check engagement, not just deliverability

A mailbox that exists but has not opened anything in a year is a complaint waiting to happen and, at some providers, a recycled spam trap waiting to be created. Set an engagement window that fits your cadence, move addresses past it into a re-permission segment, and stop sending to the ones that do not respond.

## 7. Authenticate before you send

SPF, DKIM and DMARC are required for bulk mail at Gmail and Yahoo, and a `550 5.7.x` reply from a receiver is about your authentication, not the recipient. Check your records before the campaign, and check the DMARC policy of your own domain is at least `p=none` with a reporting address.

## 8. Warm up the segments you are unsure about

Send the confirmed deliverable segment first, then the risky segments in small daily batches while watching bounces and complaints. A catch-all segment that bounces at 5% needs a lower confidence threshold, not a bigger batch.

## 9. Feed outcomes back

Point your sending provider's bounce webhook at Spaw, or post delivered and bounced events yourself. Hard bounces join the suppression list automatically, deliveries turn later lookups into confirmed mailboxes, and the per-domain bounce rate feeds the confidence score for every catch-all address at that domain. The dashboard shows measured accuracy: your outcomes crossed with the verdicts on file.

## 10. Keep it clean with a monitor

Lists decay. Schedule a monitor on the list from the dashboard and it re-verifies on a cadence, applies the suppression list, and alerts you when the deliverable share drops. Repeats of an address inside seven days are free, and undeliverable answers are always free, so the ongoing cost is the share of the list that is still good.

## What does it cost?

| Item | Credits |
| --- | --- |
| Duplicate of an address already in the run | 0 |
| Address on your suppression list | 0 |
| Undeliverable verdict | 0 |
| Deliverable or risky verdict | 1 |
| Repeat of a charged lookup within 7 days | 0 |
| Monthly free grant, every account | +10 |

A 50,000-address list that is 20% dead and 5% duplicates costs at most 37,500 credits to verify once, and the monitor that keeps it clean costs only for addresses whose answer changes.

## What to do next

- Create the [bulk job](/docs/api/create-bulk-job) with your file as it is; the columns come back with the verdicts appended.
- Import your platform's bounce export into the [suppression list](/docs/api/add-suppressions) first.
- Decide the segment policies with the [role](/guides/role-based-email-addresses) and [disposable](/guides/disposable-email-domains) guides.
- Set targets using the [bounce rate benchmarks](/guides/email-bounce-rate-benchmarks) and the mailbox providers' own thresholds.

Reference: https://spaw.co/guides/email-list-cleaning-checklist
