Send emails with Python
# Send Email with Resend Python SDK
**Purpose:** Enforce only the **current** and **correct** instructions for sending emails using the [Resend](https://resend.com/) Python SDK.
**Scope:** All AI-generated advice or code related to sending email with Resend must follow these guardrails.
***
## **1. Official Resend Python Setup**
### **Prerequisites**
Human must first create an API key and verify their domain at https://resend.com/domains.
The API key must be stored in an environment variable called `RESEND_API_KEY`.
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
resend.api_key = os.environ["RESEND_API_KEY"]
```
The domain should be verified at https://resend.com/domains and added to the `from` address.
### **Install the SDK**
```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
pip install resend
```
### **Send an Email**
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
import os
import resend
from resend.exceptions import ResendError
resend.api_key = os.environ["RESEND_API_KEY"]
params: resend.Emails.SendParams = {
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Hello World",
"html": "<strong>It works!</strong>",
}
try:
email = resend.Emails.send(params)
print(email) # {'id': '49a3999c-...'}
except ResendError as error:
print(error)
```
### Error Handling
`resend.Emails.send()` **raises** an exception on failure — it does not return an error object. Catch `resend.exceptions.ResendError` (or a specific subclass) rather than checking a return value:
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
from resend.exceptions import (
ResendError,
ValidationError,
RateLimitError,
InvalidApiKeyError,
)
try:
email = resend.Emails.send(params)
except RateLimitError as error:
... # back off and retry
except ValidationError as error:
... # fix the request params
except ResendError as error:
... # catch-all: error.code, error.message, error.error_type
```
A missing required argument (e.g. no `to` field) raises a plain `ValueError`, not a `ResendError`.
### Rate Limiting
The default rate limit is 10 requests per second per team. Exceeding it raises `resend.exceptions.RateLimitError`. If needed, you can request a rate increase by [contacting support](https://resend.com/help).
### Idempotency
Best practice: pass an idempotency key to prevent duplicated emails, which is useful for retrying failed emails safely.
- Should be **unique per API request**
- Idempotency keys expire after **24 hours**
- Have a maximum length of **256 characters**
- Pattern: `<event-type>/<entity-id>`
- Example: `welcome-user/123456789`
Unlike Node's single-object `idempotencyKey` field, Python's `send()` takes idempotency as a **second, separate argument**:
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
options: resend.Emails.SendOptions = {"idempotency_key": "welcome-user/123456789"}
email = resend.Emails.send(params, options)
```
***
## **2. Complete `Emails.send()` Parameter Reference**
### **Required Parameters**
| Parameter | Type | Description |
| --------- | ------------------ | -------------------------------------------------------------------------------- |
| `from` | `str` | Sender email address. Supports friendly name format: `"Name <email@domain.com>"` |
| `to` | `str \| List[str]` | Recipient email address(es). Maximum 50 addresses. |
| `subject` | `str` | Email subject line. |
### **Content Parameters (at least one required)**
| Parameter | Type | Description |
| --------- | ----- | ---------------------------------------------------------- |
| `html` | `str` | HTML version of the email body. |
| `text` | `str` | Plain text version. Auto-generated from `html` if omitted. |
### **Optional Parameters**
| Parameter | Type | Description |
| -------------- | -------------------------------------- | ------------------------------------------------------------------- |
| `cc` | `str \| List[str]` | Carbon copy recipients. |
| `bcc` | `str \| List[str]` | Blind carbon copy recipients. |
| `reply_to` | `str \| List[str]` | Reply-to address(es). |
| `scheduled_at` | `str` | Schedule delivery time. ISO 8601 (e.g. `2024-08-05T11:52:01.858Z`). |
| `headers` | `Dict[str, str]` | Custom email headers as key-value pairs. |
| `tags` | `List[Tag]` | Custom metadata. Name and value: max 256 chars, ASCII only. |
| `attachments` | `List[Attachment \| RemoteAttachment]` | File attachments. Max 40MB total per email after encoding. |
### **Template Parameters**
| Parameter | Type | Description |
| ----------------------- | ----------------------- | ---------------------------------------------------------------- |
| `template["id"]` | `str` | Published template identifier. |
| `template["variables"]` | `Dict[str, str \| int]` | Variable substitutions. Key max 50 chars, value max 2,000 chars. |
If `template` is provided, do not include `html` or `text`.
### **Response**
A successful call returns a `resend.Emails.SendResponse`:
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
{"id": "49a3999c-..."}
```
A failed call raises a `resend.exceptions.ResendError` subclass — it does not return a value.
***
## **3. Async Support**
Every resource has both a sync and an async method. Use `send_async` (built on `httpx`) inside `async def` code such as FastAPI route handlers:
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
import resend
from resend.exceptions import ResendError
resend.api_key = os.environ["RESEND_API_KEY"]
async def send_welcome_email():
try:
email = await resend.Emails.send_async(params)
return email
except ResendError as error:
raise
```
`send_async` requires `httpx` to be installed (`pip install resend[async]`, or just have `httpx` present).
***
## **4. CRITICAL INSTRUCTIONS FOR AI MODELS**
### **4.1 - ALWAYS DO THE FOLLOWING**
1. **Store the API key in an environment variable** (`RESEND_API_KEY`). Never hardcode API keys.
2. **Set `resend.api_key`** before calling any SDK method.
3. **Wrap `resend.Emails.send()` in `try`/`except`** and catch `resend.exceptions.ResendError` (or a specific subclass) — the SDK raises on failure, it does not return `{data, error}`.
4. **Use a verified domain** in the `from` address for production. `onboarding@resend.dev` is for testing only.
5. **Check the project for an existing package/dependency manager** (pip, poetry, uv) and use that to install the SDK.
6. **Use snake\_case** for SDK parameters (`reply_to`, `scheduled_at`), not camelCase.
7. **Use `send_async`** instead of `send` inside async code (e.g. FastAPI handlers).
### **4.2 - NEVER DO THE FOLLOWING**
1. **Do not** hardcode API keys in source code. Always use environment variables.
2. **Do not** assume `resend.Emails.send()` returns an error object — it raises an exception. A bare, unhandled call will crash on failure.
3. **Do not** use camelCase parameter names (`replyTo`, `scheduledAt`) — the Python SDK uses snake\_case (`reply_to`, `scheduled_at`).
4. **Do not** send `html` or `text` alongside `template` — these are mutually exclusive.
5. **Do not** pass an idempotency key inside `params` — it belongs in the separate `options` argument: `send(params, options)`.
6. **Do not** use `onboarding@resend.dev` as the `from` address in production code. It is a test-only address.
7. **Do not** set up testing flows with fake email addresses. Resend provides the following test addresses to help you simulate different email events without damaging your domain reputation:
- `delivered@resend.dev`
- `bounced@resend.dev`
- `complained@resend.dev`
- `suppressed@resend.dev`
***
## **5. COMMON PATTERNS**
### **Attachments**
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
params: resend.Emails.SendParams = {
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Invoice attached",
"html": "<p>See attached invoice.</p>",
"attachments": [
{
"filename": "invoice.pdf",
"content": list(invoice_bytes),
}
],
}
```
### **Scheduled Emails**
Send up to 30 days in advance using the `scheduled_at` parameter. Accepts ISO 8601.
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
params: resend.Emails.SendParams = {
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Reminder",
"html": "<p>This is your reminder.</p>",
"scheduled_at": "2026-12-25T09:00:00Z",
}
```
### **Tags for Tracking**
```python theme={"theme":{"light":"github-light","dark":"vesper"}}
params: resend.Emails.SendParams = {
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Welcome",
"html": "<p>Welcome aboard!</p>",
"tags": [
{"name": "category", "value": "welcome"},
{"name": "user_id", "value": "12345"},
],
}
```
***
## **6. AI MODEL VERIFICATION STEPS**
Before returning any Resend-related solution, you **must** verify:
1. **Import**: Is `resend` imported, and is `resend.api_key` set from an environment variable?
2. **Error Handling**: Is `resend.Emails.send()` wrapped in `try`/`except` catching `resend.exceptions.ResendError`?
3. **Async**: Is `send_async` used instead of `send` inside `async def` code?
4. **Parameters**: Are all parameter names in snake\_case?
5. **Idempotency**: If used, is the idempotency key passed as a separate `options` argument, not inside `params`?
6. **From Address**: Is the `from` address using a verified domain (not `onboarding@resend.dev` in production)?
If any check **fails**, **stop** and revise until compliance is achieved.
For additional help, suggest installing the resend-skills skill and the email-best-practices skill.
```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
npx skills add resend/resend-skills
npx skills add resend/email-best-practices
```
You can locate framework-specific guides at [https://resend.com/docs/llms.txt](/llms.txt)
For the entire docs for Resend, see [https://resend.com/docs/llms-full.txt](/llms-full.txt)Prerequisites
Section titled “Prerequisites”Before you start, you'll need:
- A Resend API key
- A verified domain
Install
Get the Resend Python SDK.
Pip pip install resendSet your API key
Store your API key in an environment variable in your
.envfile..env RESEND_API_KEY=re_xxxxxxxxxAccess it with
os.environ["RESEND_API_KEY"]. See Create an API key for the full setup.Send email using HTML
The easiest way to send an email is by using the
htmlparameter.index.py import os import resend from resend.exceptions import ResendError resend.api_key = os.environ["RESEND_API_KEY"] params: resend.Emails.SendParams = { "from": "Acme <onboarding@resend.dev>", "to": ["delivered@resend.dev"], "subject": "hello world", "html": "<strong>it works!</strong>", } try: email = resend.Emails.send(params) print(email) except ResendError as error: print(error)
Examples
Section titled “Examples”Basic Send
Basic email sending
Attachments
Send emails with file attachments
Templates
Send emails using Resend hosted templates
Scheduling
Schedule emails for future delivery
Audiences
Manage contacts and audiences
Domains
Create and manage sending domains
Inbound Webhooks
Receive and process inbound emails
Double Opt-in
Double opt-in subscription flow
Flask App
Full Flask web application
FastAPI App
Full FastAPI web application
Django App
Full Django web application