# Send emails with Python

> Send email from Python with the official sendping package — zero dependencies, configured module-level like stripe or resend.

The official [`sendping`](https://pypi.org/project/sendping/) package is the fastest way to send email from Python. It has **zero dependencies** (standard library `urllib` + `json` only) and is configured module-level — set `sendping.api_key` once, then call the resource classes. There is no client object to construct and pass around.

This guide sends a single email with `sendping.Emails.send(...)`. The package also sets the required `User-Agent` header on every request for you, retries `429`/`503` for you, and raises typed errors instead of handing back raw HTTP.

## Prerequisites

1. A **verified domain** for your `from` address. ([guide](https://www.sendping.co/docs/domains/managing))
2. An **API key** (`mb_...`), read from `SENDPING_API_KEY`. ([Authentication](https://www.sendping.co/docs/authentication))
3. Python **3.8 or newer** — the package declares `requires-python >= 3.8`.

## Install

```sh
pip install sendping
```

## Configure

Assign your key to `sendping.api_key` once, at import time or during app startup. Read it from the environment so the secret never lands in source control.

**mailer.py**

```python
import os

import sendping

sendping.api_key = os.environ["SENDPING_API_KEY"]
```

Two more module-level knobs are optional: `sendping.timeout` (per-request seconds, default `30`) and `sendping.max_retries` (automatic retries of `429`/`503` only, default `2`; set `0` to disable). Because only those two statuses are retried, a send is never duplicated by a retry.

## Send an email

Every field of the send body is a plain dict key — `from` is a Python keyword, so the payload is a dict rather than keyword arguments. `to` accepts a single address or a list.

**send.py**

```python
import os

import sendping

sendping.api_key = os.environ["SENDPING_API_KEY"]

email = sendping.Emails.send({
    "from": "Acme <hello@yourdomain.com>",
    "to": ["delivered@test.sendping.co"],
    "subject": "Hello from Python",
    "html": "<p>Sent with the sendping package 🐍</p>",
})

print("Sent email", email["id"])
```

> **Note:** `delivered@test.sendping.co` is the SendPing simulator: the send is accepted, recorded, and marked delivered without touching a real inbox. Swap it for your own address once the first call succeeds — and never use `@example.com`, which the API rejects with `422`.

## Handling the response

Every method returns the parsed JSON response as a dict, so a successful send gives you `email["id"]`. A non-2xx answer raises `sendping.SendPingError`, which carries the `{ statusCode, name, message }` envelope — branch on `err.name`, never on `err.message` (messages are scrubbed server-side and are not a stable contract).

```python
import time

try:
    email = sendping.Emails.send(params)
    print("Sent email", email["id"])
except sendping.SendPingError as err:
    print(err.status_code, err.name, err.message)

    if err.name == "daily_quota_exceeded":
        # Plan/quota rejections describe the cap that was hit.
        print(err.limit["used"], err.limit["limit"], err.limit["next_plan"])

    if err.retry_after:
        time.sleep(err.retry_after)

    print(err.body)   # the full parsed error body
```

A transport failure (or a missing key) raises the same exception with `status_code == 0` and `name` of `network_error` or `missing_api_key`, so one `except` clause covers both.

## Retry safely with an idempotency key

Pass `options={"idempotency_key": "..."}` so replaying a request returns the original response instead of sending twice. Only `Emails.send` and `Batch.send` honour it — see [Idempotency](https://www.sendping.co/docs/emails/idempotency).

```python
sendping.Emails.send(params, options={"idempotency_key": "order-123"})

# Up to 100 emails in one request (no attachments, no scheduled_at).
sendping.Batch.send([
    {"from": "hello@yourdomain.com", "to": ["delivered@test.sendping.co"], "subject": "Hi A", "html": "<p>A</p>"},
    {"from": "hello@yourdomain.com", "to": ["delivered@test.sendping.co"], "subject": "Hi B", "html": "<p>B</p>"},
], options={"idempotency_key": "orders-2026-08-19"})
```

## Schedule instead of sending now

Add `scheduled_at` (ISO 8601) to hand the send to the scheduler. A scheduled send goes through the real delivery path, so it cannot target the simulator address — use one of your own recipients.

```python
scheduled = sendping.Emails.send({
    "from": "Acme <hello@yourdomain.com>",
    "to": ["you@yourdomain.com"],
    "subject": "Tomorrow morning",
    "html": "<p>Queued ahead of time.</p>",
    "scheduled_at": "2026-09-01T09:00:00Z",
})

sendping.Emails.update(scheduled["id"], {"scheduled_at": "2026-09-01T14:00:00Z"})   # reschedule
sendping.Emails.cancel(scheduled["id"])                                            # or call it off
```

> **Warning:** Keep the `mb_` API key server-side — load it from an environment variable or secrets manager, never commit it, and never ship it in a client-side script or notebook you share. Anyone holding the key can send email as you.

## Next steps

- See the full [Send Email API](https://www.sendping.co/docs/api/emails-send) reference for every body field.
- Using a web framework? See [Send emails with Django](https://www.sendping.co/docs/integrations/django).
- Explore [the SDKs reference](https://www.sendping.co/docs/resources/sdks) for the other resources the package exposes — `Domains`, `Contacts`, `Campaigns`, `Automations`, `Webhooks`, and more.
- Want no dependency at all? [Python without the SDK](https://www.sendping.co/docs/send-with/python) does the same send with `requests` or stdlib `urllib`.
