Metadata-Version: 2.5
Name: proofwire
Version: 0.1.0
Summary: Email, phone and IP validation with three-state verdicts and the evidence behind them.
Project-URL: Homepage, https://proofwire.app
Project-URL: Documentation, https://proofwire.app/docs
Project-URL: Source, https://github.com/Perseo1988/proofwire-sdk-python
Project-URL: Issues, https://github.com/Perseo1988/proofwire-sdk-python/issues
Author: Arcangelo Sternativo
License: MIT License
        
        Copyright (c) 2026 Arcangelo Sternativo
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: catch-all,deliverability,email-validation,email-verification,phone-validation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# proofwire

Email, phone and IP validation for Python.

```bash
pip install proofwire
```

```python
from proofwire import Proofwire

proofwire = Proofwire()  # reads PROOFWIRE_API_KEY

result = proofwire.email("someone@example.com")

action = result.match(
    valid=lambda r: "send",
    invalid=lambda r: "drop",
    unknown=lambda r: "ask them to confirm the address",
)
```

No dependencies. It gets installed into other people's services, and a
validation client that drags in a transitive tree is one that eventually breaks
a build for reasons unrelated to validation.

## The one design decision worth knowing

There is no `result.valid`.

That attribute would be the most convenient thing this package could offer and
the most damaging, because `if result.valid:` files every inconclusive answer
under "not valid" — and inconclusive is the case the product exists to
surface. A large minority of business mail servers accept every address you ask
about, so nothing observable distinguishes a real mailbox from a fictional one.
Most validators resolve that into "valid" and invoice you. You find out when it
bounces.

So verdicts have three states, and `match` takes all three as required
keyword-only arguments. Leave one out and it fails at the call site:

```
TypeError: match() missing 1 required keyword-only argument: 'unknown'
```

Python cannot check exhaustiveness the way a compiler can. This is the closest
thing that fails early and loudly rather than three weeks later in a bounce
report.

Inconclusive verdicts are never billed. You are not paying for the honesty.

## What a result carries

```python
result = proofwire.email("j.smith@thecompany.com")

result.verdict                    # 'unknown'
result.confidence                 # 0.52
result.attributes["catchAll"]     # True
result.billing.credits_charged    # 0
result.billing.reason             # why it cost what it did, in plain language
result.is_inconclusive            # True

print(result.explain())
```

```
UNKNOWN - j.smith@thecompany.com
confidence 52%, risk 10/100, 0 credits (inconclusive verdicts are not billed)
  [syntax] Address is syntactically well formed. (+0.80)
  [mx] 1 MX record published. (+1.90)
  [smtp] Control probe: the server also accepted an address that cannot exist,
         so its acceptance carries no information. (-1.40)
```

`explain()` is written to be pasted into a log line or a reply to a customer
asking why you would not send to them.

The control probe is the part worth noticing. Before trusting an acceptance,
the server is asked about an address that cannot exist. If that is accepted
too, the acceptance of the real one means nothing, and the verdict says so.

## Retries and double charges

Every call carries an idempotency key, generated for you. A retry after a
timeout replays the original response instead of spending again, so the client
retries on 5xx and 429 by default without risking a double charge. A 429 is
respected by the header it came with, not by a guess.

Pass your own key when a retry has to survive a process restart:

```python
proofwire.email(address, idempotency_key=f"signup:{user_id}")
```

## Errors

Separated by what you should do about them, because a malformed key is a deploy
problem, an empty balance is a billing problem, and a 502 is a wait-and-retry
problem.

| | |
|---|---|
| `AuthenticationError` | Key missing, malformed or revoked |
| `InsufficientCreditsError` | Out of credits, or past your spend cap |
| `InvalidRequestError` | The value or the request is wrong |
| `RateLimitError` | Carries `retry_after_seconds` |
| `ServiceError` | Our side; already retried |
| `ConnectionError` | Never reached us |

An inconclusive verdict is not among them. It is a successful response.

## Configuration

```python
Proofwire(
    api_key="pk_live_...",   # or PROOFWIRE_API_KEY
    timeout=15.0,
    max_retries=2,
)
```

A `pk_test_` key answers from fixed sandbox fixtures and is never billed, which
is what makes it usable in a test suite. `proofwire.is_test_mode` tells you
which kind you have.

## Links

- [Documentation](https://proofwire.app/docs)
- [Published accuracy benchmark](https://proofwire.app/benchmark), with the
  dataset downloadable so you can rerun it
- [MCP server](https://proofwire.app/mcp), if you are wiring this into an agent
- [Source and issues](https://github.com/Perseo1988/proofwire-sdk-python)

MIT.
