# Quickstart: Python

> Send email from Python with one requests call: POST to the Email Fast REST API with a sandbox key, add an idempotency key, then read the event timeline.

Canonical: https://emailfast.dev/docs/quickstart-python

## 1. Send

The API is plain JSON over HTTPS with bearer auth, so `requests` is all you need.
(A generated Python SDK covers the same surface — see [the docs](/docs) — but the
raw calls are two lines longer and hide nothing.)

```python
import os
import requests

API = "https://api.emailfast.dev"
headers = {"Authorization": f"Bearer {os.environ['EMAILFAST_API_KEY']}"}  # ef_sandbox_… works everywhere

r = requests.post(f"{API}/v1/emails", headers=headers, json={
    "to": "ada@example.com",
    "subject": "Welcome to the ledger",
    "html": "<h1>Hi {{name}}</h1><p>Your account is ready.</p>",
    "data": {"name": "Ada"},
    "idempotency_key": "welcome-ada-1",  # retry-safe: same key can never double-send
})
r.raise_for_status()
msg = r.json()
print(msg["id"], msg["status"])  # "msg_…", "queued"
```

a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send. That is why the `idempotency_key` matters: wrap this call
in any retry loop you like — `urllib3.Retry`, tenacity, a bare `for` — and a
timeout after the server committed the send still can't produce a duplicate.

## 2. Watch it happen

```python
timeline = requests.get(f"{API}/v1/messages/{msg['id']}", headers=headers).json()
print([e["type"] for e in timeline["events"]])  # admitted → rendered → (sandbox-)delivered …
```

With a sandbox key the message lands in your hosted capture inbox — same
pipeline, nothing sent: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.

## 3. Batch, if you need it

```python
r = requests.post(f"{API}/v1/emails/batch", headers=headers, json={
    "messages": [
        {"to": "a@example.com", "subject": "Hi", "html": "<p>A</p>"},
        {"to": "b@example.com", "subject": "Hi", "html": "<p>B</p>"},
    ],
    "idempotency_key": "digest-2026-07-17",  # derived per recipient — partial retries stay safe
})
```

Up to 500 per call, with per-item results.

## Next

- [The raw REST reference](/docs/quickstart-curl)
- [Templates & MJML](/features/templates)
- [Migrating from SendGrid/Mailgun/Postmark](/features/migration)
