Metadata-Version: 2.5
Name: faivelo
Version: 0.2.0
Summary: Official Faivelo SDK for Python: email inboxes for your code and your AI agents. Send, receive, drafts, labels, threads, domains, DNS and webhooks.
Project-URL: Homepage, https://faivelo.com
Project-URL: Documentation, https://faivelo.com/docs/api
Author-email: Faivelo <support@faivelo.com>
License-Expression: MIT
License-File: LICENSE
Keywords: ai agents,email,faivelo,imap,inbox,mailbox,smtp,transactional
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications :: Email
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.23
Description-Content-Type: text/markdown

# Faivelo Python SDK

The official [Faivelo](https://faivelo.com) 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

```bash
pip install faivelo
```

Requires Python 3.9 or newer. Fully typed; the only dependency is [httpx](https://www.python-httpx.org).

## Setup

Create an API key in your Faivelo dashboard under Settings → Developers, then:

```python
from faivelo import Faivelo

faivelo = Faivelo("fvl_live_xxxxxxxx")  # or set FAIVELO_API_KEY and call Faivelo()
```

For asyncio, `AsyncFaivelo` has the same methods:

```python
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

```python
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

```python
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:

```python
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

```python
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:

```python
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:

```python
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.

```python
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.

```python
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.

```python
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.

```python
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:

```python
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

```python
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:

```python
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:

```python
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

```python
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:

```bash
python scripts/gen_sync.py
python -m unittest discover -s tests
```

## License

MIT
