precleared
Python client for Precleared, a cold-email legality check for AI agents.
One call per recipient, before the first cold email, answers: is it legal to send this person a cold or marketing email? Precleared looks the recipient up live (for the UK: Companies House, the domain and the company website) and applies the local rules (UK PECR and GDPR, US CAN-SPAM, Canadian CASL, German UWG, French CNIL guidance and the rules for Australia, Ireland and the Netherlands). It returns a verdict, the rule that decided it with its source, what the footer must include, the next step and a signed receipt.
Decision support with sources, not legal advice.
Install
pip install precleared # client only (depends on httpx)
pip install "precleared[langchain]" # + LangChain / LangGraph tools
pip install "precleared[crewai]" # + CrewAI tools
pip install "precleared[llamaindex]" # + LlamaIndex tools
pip install "precleared[openai-agents]" # + OpenAI Agents SDK tools
pip install "precleared[all]"
Python 3.9 or later for the client. The framework extras follow the frameworks' own Python requirements (CrewAI and the current LangChain, LlamaIndex and Agents SDK releases need 3.10 or later).
Quick start
from precleared import Precleared
client = Precleared() # keyless: 10 checks a day per network. Or Precleared("pc_live_...")
result = client.check("jane@acme.co.uk", pitch="Bookkeeping software for small firms", address_source="unknown")
if result.ok:
print("Send, with a footer that meets:", result.footer_text)
else:
print(result.verdict, result.next_step, result.receipt_url)
check() takes the same fields as the API: recipient_email or recipient_domain, pitch, address_source (published, bought, scraped, given or unknown), sender_country (default GB), existing_customer, pitch_relevant, company_number, recipient_country, default_recipient_country and suppressed.
address_source defaults to unknown and is always sent (the API treats a missing value as unknown too). It changes verdicts, so never pass published unless the recipient put the address on their website or a public listing for people to contact them.
Reading a result
| Verdict | Meaning | result.ok |
result.must_not_send |
|---|---|---|---|
ok_to_email |
Send, with a footer that meets footer_text |
True |
False |
consent_needed |
Do not send. Follow next_step |
False |
True |
blocked |
Do not send. Follow next_step |
False |
True |
unknown |
A fact could not be established. Treat as consent_needed unless you can supply the missing fact (for example company_number or recipient_country) and check again |
False |
False (see needs_more_facts) |
footer_text describes a requirement (who you are, a postal address, a way to opt out). It is not wording to paste.
CheckResult is a dataclass with typed fields (verdict, confidence, reasons, rules_applied, footer_text, next_step, facts, receipt, usage, disclaimer, rules_version) plus:
.raw: the full response as a dict.rule:rule_keyof the reason that decided the verdict.reason: that reason, withtext,source_titleandsource_url.notes: the other reasons entries.receipt_idand.receipt_url(https://precleared.dev/r/<id>).summary()and.summary_text(): the compact forms the framework tools return
Receipts and health
receipt = client.verify_receipt("cc_rcpt_...") # also accepts a receipt URL
receipt.verdict, receipt.request_hash, receipt.response_hash, receipt.signature, receipt.public_key
client.health().ok # GET /api/v1/health: status, checked_at, api_ok, mcp_ok, worker_ok
A receipt proves what was asked and answered. The signature is Ed25519 over request_hash.response_hash; the receipt endpoint returns the public key alongside it, and GET /api/v1/public-key serves it too.
Async
from precleared import AsyncPrecleared
async with AsyncPrecleared("pc_live_...") as client:
result = await client.check("jane@acme.co.uk", address_source="bought")
Framework tools
Each adapter exposes the same two tools, precleared_check and precleared_verify_receipt, with descriptions that tell the model when to call them: before the first cold email to anyone who is not already a customer, in the UK, US, Canada, Germany, France, Australia, Ireland or the Netherlands, and not for transactional email. The tools return compact text (verdict, rule, footer_text, next_step, receipt URL) rather than the whole JSON. Pass to_dict=True for a dictionary instead. Every factory takes an optional api_key.
Example tool output:
verdict: consent_needed (confidence: medium)
rule: gb_individual_consent
reason: The recipient is a sole trader or a partnership in England, Wales or Northern Ireland. PECR treats them as individual subscribers, so unsolicited marketing email needs their prior consent, or the soft opt-in for existing customers. Source: PECR 2003 regulation 22(2) and ICO Guide to PECR, https://www.legislation.gov.uk/uksi/2003/2426/regulation/22
next_step: Do not cold email. Use a route they offer, such as their contact form, or obtain consent first.
receipt: cc_rcpt_E21TTJV8D9JAYTQZ https://precleared.dev/r/cc_rcpt_E21TTJV8D9JAYTQZ
API errors come back as text too (error: daily_limit, with get_key_url, buy_url or retry_after when the API gives one), so the agent can react instead of crashing.
LangChain / LangGraph
from langchain.agents import create_agent # pip install langchain
from precleared.langchain import precleared_tools
agent = create_agent(model, tools=precleared_tools(api_key="pc_live_..."))
precleared_tools() returns two StructuredTools with pydantic argument schemas, so they also work with model.bind_tools(...) and LangGraph's prebuilt agents. precleared_check_tool() and precleared_verify_receipt_tool() build them one at a time.
CrewAI
from crewai import Agent
from precleared.crewai import PreclearedCheckTool, PreclearedVerifyReceiptTool
sdr = Agent(role="SDR", goal="...", backstory="...", tools=[PreclearedCheckTool(api_key="pc_live_..."), PreclearedVerifyReceiptTool()])
The key is held privately, so it does not appear in the tool's repr() or model_dump().
LlamaIndex
from llama_index.core.agent.workflow import FunctionAgent
from precleared.llamaindex import precleared_tools
agent = FunctionAgent(tools=precleared_tools(), llm=llm)
OpenAI Agents SDK
from agents import Agent
from precleared.openai_agents import precleared_tools
agent = Agent(name="SDR", instructions="Check every address before the first email.", tools=precleared_tools())
Errors
Everything raised after a request is sent is a subclass of precleared.PreclearedError, with .message, .status_code, .code and .body. Bad arguments raise a plain ValueError before anything is sent (for example an address_source outside the five allowed values).
| Class | When | Extra attributes |
|---|---|---|
InvalidRequest |
400, the body names the invalid field | |
InvalidKey |
401, invalid_key or revoked_key |
.code |
PaymentRequired |
402, free checks used and no pack credits | .buy_url |
DailyLimit |
429 daily_limit, the free daily allowance is used |
.get_key_url |
RateLimited |
429 rate_limited, more than 60 checks a minute |
.retry_after (seconds, from the body or the Retry-After header) |
ReceiptNotFound |
404 from the receipt endpoint | |
ServerError |
5xx | |
NetworkError |
the request did not complete (DNS, connection, timeout) | __cause__ is the httpx exception |
from precleared import Precleared, DailyLimit, RateLimited
try:
result = Precleared().check("jane@acme.co.uk")
except RateLimited as exc:
time.sleep(exc.retry_after or 1)
except DailyLimit as exc:
print("Get a free key:", exc.get_key_url)
The API key is never logged, printed or included in an exception.
Allowances and limits
- Without a key: 10 checks a day over the REST API, shared by everyone on the same network.
- With a free key (
https://precleared.dev/get-key): 25 checks a day over the REST API. - 60 checks a minute per key. Above that the API returns 429
rate_limitedwithretry_after. - Days run on UK time. After the free checks, checks use pack credits: a pack is £39 for 2,000 checks, packs never expire and stack on the same key. When no credits are left the API returns 402 with
buy_url. - A check can take up to 25 seconds when a register or website is slow; the client's default timeout is 30 seconds.
Configuration
Precleared(
api_key=None, # or the PRECLEARED_API_KEY environment variable
base_url="https://precleared.dev",
timeout=30, # seconds, or an httpx.Timeout
user_agent="precleared-python/0.1.0", # Precleared's hosting refuses requests without one
)
Disclaimer
Decision support with sources, not legal advice. The receipt proves what was asked and answered, not that an email was sent.
Support
support@precleared.dev. API reference: https://precleared.dev/docs and https://precleared.dev/openapi.json.
Development
pip install -e . pytest anyio
pytest
The tests mock all HTTP with httpx.MockTransport and never call the live API. The adapter tests run when the matching framework is installed and skip otherwise.
Metadata
Release files for precleared 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| precleared-0.1.0.tar.gz | 39.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| precleared-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.4 kB
Release files / precleared-0.1.0.tar.gz
| Download URL | precleared-0.1.0.tar.gz |
|---|---|
| Size | 39.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eb7c9c9d7f35f161480d5481fdae64db206d33ab3266795301e05f2b18a0eab3
|
|
BLAKE2b-256 checksum How to use checksums |
80f79e14f391e0998d82cf1583469fe01da5dda63e8661951244967a5eae4411
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / precleared-0.1.0-py3-none-any.whl
| Download URL | precleared-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f3c2d81f030cf44078a22447e555beb826d59ad7bc87c67528f972629b943021
|
|
BLAKE2b-256 checksum How to use checksums |
14d436aaa434d2ab4afa68b72b6bd4d9ef269ebcec259590e346864c3d8dfc8f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logSecurity for precleared 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for precleared-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.