Use case: email validation at signup

# Check every email address before the account is created

Call the API when the signup form is submitted. You get a verdict, a risk score from 0 to 100, and the result of each check. Your code decides what happens next.

## Where the check goes

Run it inline, at form submit, before the account is created. That is the point where a bad signup can still be stopped from becoming a row in your database. A check that runs after the account exists answers a different question: by then a session may be active and a trial may have started.

## The request

```bash
curl -X POST https://pingvalid.com/v1/validate \
  -H "Authorization: Bearer pv_live_…" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"email":"jane@example.com"}'
```

Create the key in your dashboard under API keys, after you have verified your email address. `tier` is an optional field in the body: `quick`, `standard` or `deep`. `standard` is the default.

The response carries `verdict`, `risk_score`, `tier_results` (one entry per check that ran, each with an `outcome` of `pass`, `fail` or `unknown`), `cached_hit`, `request_id` and `cost_credits`. A full real response and a complete signup handler are in [how to stop fake signups](/blog/stop-fake-signups-saas). The [API reference](/docs/api) has samples in curl, Python, Node, PHP and Go.

## What to do with each verdict

| Verdict | What it means | What a signup form does |
|---|---|---|
| `valid` | The address passed the checks. | Create the account. |
| `invalid` | A blocking check failed (bad syntax, no domain, no MX, mailbox rejected), the address is on a disposable domain, or the risk signals add up to more than 70 (on Deep, a role account on a catch-all domain scores 75). | Reject the signup and ask for a different address. |
| `risky` | The mailbox may exist, but a check raised the risk score: a role account, or a catch-all domain on the Deep tier. | Create the account in a lower-trust state, for example pending email confirmation. |
| `unknown` | A check could not finish, for example because the receiving server asked to retry later. | Let the signup through pending email confirmation, and check again later. You are not charged. |

Blocking on `invalid` is enough to stop an address on the disposable list: a match returns `invalid` on its own. Do not reject every verdict that is not `valid`. That turns away shared company mailboxes and addresses whose check was inconclusive. On the Deep tier, a role account on a catch-all domain scores 75 and comes back `invalid`, so on Deep a reject-on-`invalid` rule also turns away some shared mailboxes.

A role account is a shared mailbox such as postmaster@. A catch-all domain accepts mail for any address, so a missing mailbox looks real. A disposable domain gives out throwaway addresses.

## What real results look like

Three rows from a test set we ran on 2 October 2026. It is a constructed set, built to show one case per row. It is not a customer list, and no accuracy figure can be drawn from it.

| Address tested | Verdict | Risk | What the evidence said |
|---|---|---|---|
| `test@guerrillamail.com` | `invalid` | 75 | Disposable domain. The mail server accepted the mailbox. |
| `postmaster@cloudflare.com` | `risky` | 40 | Role account. The mail server accepted the mailbox. |
| A made-up mailbox at `pingvalid.com` | `valid` on Standard, `risky` on Deep | 0 / 35 | Our own domain accepts mail for any address. Only the Deep tier tests for that. |

A mail server saying yes is not the verdict. The first two addresses were accepted by their servers.

## Which tier a signup form should call

| Tier | What it checks | Credits per address |
|---|---|---|
| Quick | Syntax, domain, MX records, disposable domains, role accounts | 1 |
| Standard | Everything in Quick, plus a check with the receiving mail server | 1 |
| Deep (needs a paid plan or a top-up) | Everything in Standard, plus catch-all detection | 2 |

Standard is the default, and the only Free tier that asks the mail server about the mailbox. Call Quick if you do not want to wait on another server inside the signup request: Quick opens no connection to the receiving mail server. Standard and Deep do. No message is sent. Whichever tier you call, set your own timeout and decide in advance what happens when it expires.

## What your code has to handle

- **`422`: the input never reached the checks.** You get it when `email` is missing, empty or longer than 320 characters, when `tier` is not `quick`, `standard` or `deep`, or when the address has no `@`, nothing on one side of it, a second unquoted `@`, or a non-ASCII domain that cannot be converted. It costs nothing. Branch on the status, not the body: the error body is not the same for every case. Any other malformed address comes back as a normal response with the verdict `invalid`, and is charged like any other check. Send `Accept: application/json`, as the sample does: without it, a missing or empty `email` gets a redirect, not a `422`.
- **`402 insufficient_credits`: the balance is below the cost of the request.** There are no overage charges. Checks are refused until you top up or upgrade. On the Free plan, the monthly top-up back to 100 also restores it.
- **`403 plan_required`: the Deep tier was requested by an account with no paid plan and no top-up.** You are not charged. Call Quick or Standard.
- **`401` and `403 forbidden`: the key is missing, revoked, or lacks the `validate:single` scope.** Two errors use `403`; read `error.code` to tell them apart.
- **`429`: you passed the rate limit**, which is 1,000 requests a minute per API key.
- **Your own timeout.** Decide whether a check that fails or times out lets the signup through in a lower-trust state, or stops it. Make that choice before you ship.
- **Retries.** Send an `Idempotency-Key` header and reuse the same value when you retry the same request. A retry is then not charged twice.
- **The key.** Create the key your signup handler uses with the `validate:single` scope and no other.

## What it costs

One check uses 1 credit on Quick or Standard and 2 on Deep.

| Signups you check in a month | Tier | Credits | Plan that covers it |
|---|---|---|---|
| 100 | Quick or Standard | 100 | Free: 100 validations a month, no card |
| 5,000 | Standard | 5,000 | Starter: $19 a month |
| 50,000 | Standard | 50,000 | Growth: $79 a month |

- `unknown` verdicts are not charged.
- A repeat check served from cache is free on Quick and Standard, and 1 credit on Deep.
- Credits do not expire. On a paid plan, each renewal adds the plan's credits to what you have left. On the Free plan the balance is topped back up to 100 once a month; it does not add up.
- No annual lock-in.

The full table is on the [pricing page](/pricing).

## What it does not do

- **It does not stop one person using many ordinary mailboxes.** Validation checks an address. It cannot see that one person is behind ten of them. That takes rate limits, device signals or account-linking rules.
- **It does not catch a throwaway domain that is not on the list.** The disposable check is a list of domains. Require email confirmation before you grant anything worth abusing.
- **It is not a consent check.** A passing address says nothing about whether the person agreed to receive email from you.
- **It is not a plugin.** PingValid has no plugin for WordPress, Shopify or form builders. Adding the check to a form needs a developer.

## Other ways to run the same checks

- **A whole list.** [Upload a CSV in the dashboard](/use-cases/email-list-cleaning).
- **Inside Claude.** [Add PingValid as a connector](/use-cases/email-list-cleaning-in-claude) and ask Claude to validate addresses.
- **From an AI agent.** The same check is available as a tool over MCP. Setup is in the [MCP docs](/docs/mcp).

## Questions

**Can I use the API on the Free plan?**
Yes. The Free plan includes API keys and 100 validations a month on the Quick and Standard tiers. The Deep tier needs a paid plan or a top-up.

**Do I need the Deep tier at signup?**
Only if made-up addresses at catch-all domains are a problem for your product. On a catch-all domain, Standard returns `valid` for a mailbox that does not exist. Deep detects the catch-all domain and returns `risky` for an otherwise clean address on it, whether or not the mailbox exists. Requiring email confirmation covers the same case without the second credit.

**What happens if the check does not answer in time?**
That is your timeout and your decision. The handler in [how to stop fake signups](/blog/stop-fake-signups-saas) creates the account in a pending state when the check does not complete.

**Am I charged twice if I retry?**
Not if you send the same `Idempotency-Key` with the retry.
