Faivelo Python SDK
The official Faivelo SDK for Python. Give your code, or your AI agent, a real email inbox on your own domain: send and receive, drafts a person can review, labels, threads, attachments, domains and DNS, and signed webhooks.
Every inbox is a full mailbox, so a person can open the same inbox in Faivelo webmail (or any IMAP client) and see exactly what the agent sees.
Install
pip install faivelo
Requires Python 3.9 or newer. Fully typed; the only dependency is httpx.
Setup
Create an API key in your Faivelo dashboard under Settings → Developers, then:
from faivelo import Faivelo
faivelo = Faivelo("fvl_live_xxxxxxxx") # or set FAIVELO_API_KEY and call Faivelo()
For asyncio, AsyncFaivelo has the same methods:
from faivelo import AsyncFaivelo
async with AsyncFaivelo() as faivelo:
usage = await faivelo.usage.get()
Each key carries scopes (mail:read, mail:send, mailboxes:write, ...). A call fails with a 403 if the key lacks the scope it needs; the required scope is noted in every method's docstring.
Responses are plain dicts with the API's own keys (message["messageId"]). faivelo.types describes them for your editor and type checker.
Give an agent an inbox
mailbox = faivelo.mailboxes.create(domain="acme.com", local_part="agent", display_name="Acme Agent")
print(mailbox["fullAddress"]) # agent@acme.com
print(mailbox["credentials"]["password"]) # shown once: also works over IMAP/SMTP
Send
sent = faivelo.mailboxes.send(
"agent@acme.com",
to="user@example.com",
subject="Your order has shipped",
text="Tracking number 1Z999...",
idempotency_key="order-4812-shipped",
)
The message lands in the mailbox's Sent folder. idempotency_key makes a retry safe: a repeat with the same key within 24 hours returns the first result with replayed: True instead of sending again, which matters when an agent loop retries a step.
Attachments take bytes and are encoded for you:
faivelo.mailboxes.send(
"agent@acme.com",
to="user@example.com",
subject="Invoice",
text="Attached.",
attachments=[{"filename": "invoice.pdf", "content": open("invoice.pdf", "rb").read(), "content_type": "application/pdf"}],
)
Read and reply
inbox = faivelo.messages.list("agent@acme.com", limit=20)
for summary in inbox["messages"]:
if summary["seen"]:
continue
message = faivelo.messages.get("agent@acme.com", summary["uid"])
faivelo.mailboxes.send(
"agent@acme.com",
to=message["from"],
subject="Re: " + message["subject"],
text="Thanks, we are on it.",
in_reply_to=message.get("messageId"),
)
The whole conversation a message belongs to, the mailbox's own replies included:
thread = faivelo.messages.thread("agent@acme.com", uid)
for item in thread["messages"]: # oldest first
print(item["date"], item["from"], item["subject"])
Search, and download attachments:
results = faivelo.messages.search("agent@acme.com", from_="billing@supplier.com", date_after="2026-09-01")
pdf = faivelo.messages.download_attachment("agent@acme.com", uid, "invoice.pdf")
open("invoice.pdf", "wb").write(pdf["content"])
Drafts: let a person approve before it sends
A draft is a real message in the mailbox's Drafts folder. Your agent writes it, a person opens it in webmail and edits or sends it, or your code sends it once it has the go-ahead.
draft = faivelo.drafts.create(
"agent@acme.com",
to="customer@example.com",
subject="Refund approved",
text="We have refunded your order in full.",
)
current = faivelo.drafts.get("agent@acme.com", draft["uid"]) # includes edits a person made
faivelo.drafts.send("agent@acme.com", draft["uid"])
drafts.update replaces a draft's content and returns it under a new uid. drafts.list and drafts.delete do what they say.
Send later
Add send_at to schedule a send. The message waits in the mailbox's Drafts folder until its time, where a person can read it; deleting it there cancels the send.
from datetime import datetime, timedelta, timezone
result = faivelo.mailboxes.send(
"agent@acme.com",
to="customer@example.com",
subject="Following up",
text="Checking in on your order.",
send_at=datetime.now(timezone.utc) + timedelta(days=1), # or "2026-10-05T09:00:00-04:00"
)
scheduled = result["scheduled"]
faivelo.drafts.send("agent@acme.com", uid, send_at="2026-10-05T09:00:00Z") # schedule an existing draft
waiting = faivelo.scheduled.list("agent@acme.com")["scheduled"]
faivelo.scheduled.cancel("agent@acme.com", scheduled["id"]) # the draft stays in Drafts
A datetime must carry a timezone. The send goes through the same checks at its time as a send made then, with the API key that scheduled it. scheduled.list also shows the last week's finished sends: sent, cancelled, or failed with a reason.
Allow and block lists
Guardrails for software using a mailbox. An entry is an address or a domain (which covers its subdomains); a block entry always wins, and once an allow list has any entry, only what it names gets through.
faivelo.lists.replace("agent@acme.com", {
"send": {"allow": ["acme.com", "customer.com"]}, # the API and MCP may only send here
"receive": {"block": ["spammer.com"]}, # filed in Junk, never the inbox
})
faivelo.lists.add("agent@acme.com", {"receive": {"allow": ["newcustomer.com"]}})
faivelo.lists.remove("agent@acme.com", {"send": {"allow": ["customer.com"]}})
lists = faivelo.lists.get("agent@acme.com")
Send lists are checked on every API and MCP send (a refused recipient is a 403; nothing is sent). Receive lists are enforced by the mail server at delivery. Changing lists needs an account key with mailboxes:write: an agent key can read its lists but not loosen them.
Labels: keep state on the message
Labels are free-form lowercase tags. Use them to track where each message is in your agent's workflow, with no database of your own.
faivelo.messages.label("agent@acme.com", uid, add=["needs-reply"])
todo = faivelo.messages.list("agent@acme.com", label="needs-reply")
faivelo.messages.label("agent@acme.com", uid, add=["answered"], remove=["needs-reply"])
Transactional email
Send from any address on one of your verified domains, no mailbox required:
email = faivelo.emails.send(
from_="Acme <hello@acme.com>",
to="user@example.com",
subject="hello world",
html="<p>it works!</p>",
)
status = faivelo.emails.get(email["id"]) # delivery status and events
Templates designed in the dashboard are sent by alias: template_alias="receipt", variables={"name": "Ada"}.
Usage and quotas
usage = faivelo.usage.get()
print(usage["plan"], usage["sends"]["remaining"], "sends left until", usage["resetsAt"])
Webhooks
Faivelo signs every webhook. Verify the raw request body before trusting it:
from faivelo import FaiveloWebhookError, verify_webhook
@app.post("/webhooks/faivelo")
async def faivelo_webhook(request):
try:
event = verify_webhook(await request.body(), request.headers.get("x-faivelo-signature"), WEBHOOK_SECRET)
except FaiveloWebhookError:
return Response(status_code=400)
if event["type"] == "message.received":
...
return Response(status_code=200)
Pass the body exactly as received (bytes or str), not parsed JSON. Signatures older than five minutes are refused; change that with tolerance_seconds.
Everything else
| Namespace | What it does |
|---|---|
mailboxes |
list, create, get, update, delete, reset_password, send, list_folders |
messages |
list, get, search, thread, label, flag, move, mark_spam, delete, download_attachment |
drafts |
list, create, get, update, send, delete |
scheduled |
list, cancel |
lists |
get, replace, add, remove |
emails |
send, get |
aliases |
list, create, delete |
domains |
list, get |
dns |
list_records, create_record, update_record, delete_record |
drive |
list, get_download_url |
meet |
create_room |
usage |
get |
partner |
customers, domains, mailboxes, api_keys (for approved resale partners) |
Errors
Any non-2xx response raises FaiveloError with the API's message and the HTTP status:
from faivelo import FaiveloError
try:
faivelo.mailboxes.send("agent@acme.com", to="user@example.com", subject="hi", text="hello")
except FaiveloError as error:
print(error.status_code, error) # 0 means the request never reached Faivelo
Options
Faivelo(
api_key,
base_url="https://faivelo.com/api/v1",
timeout=30.0, # seconds
http_client=httpx.Client(), # your own client, for proxies or retries
)
Developing
The async client (src/faivelo/_async_client.py) is the one edited by hand. The sync client is generated from it:
python scripts/gen_sync.py
python -m unittest discover -s tests
License
MIT
Metadata
Release files for faivelo 0.2.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 | |
|---|---|---|---|
| faivelo-0.2.0.tar.gz | 31.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| faivelo-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.9 kB
Release files / faivelo-0.2.0.tar.gz
| Download URL | faivelo-0.2.0.tar.gz |
|---|---|
| Size | 31.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
59d8febbf9e003e6c0103699489239579cf6e626bc7b3046f3bb8a18796d6323
|
|
BLAKE2b-256 checksum How to use checksums |
8bbc8365837395287d76398d4b9e55c4dc9ed6dad81ec568fbd9104b5cedf326
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.3
|
Release files / faivelo-0.2.0-py3-none-any.whl
| Download URL | faivelo-0.2.0-py3-none-any.whl |
|---|---|
| Size | 30.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff0c2e36a211600c97b84e2e503bf7860f302db424975613ce1748ef9a2d4750
|
|
BLAKE2b-256 checksum How to use checksums |
67eb39a58ebcc2a72b3c68742a9cd79f8da6416419a91950eb3f028e321af25c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.3
|