# Python SDK

The official Python wrapper for the Mailjet API is published on PyPI as **`mailjet-rest`** and developed at [mailjet/mailjet-apiv3-python](https://github.com/mailjet/mailjet-apiv3-python).

## Compatibility

- Python **>= 3.10, < 3.15**
- Runtime dependency: `requests >= 2.33.0`
- Build backend: `setuptools`, `wheel`, `setuptools-scm`
- Test dependency: `pytest >= 9.0.3`


## Installation

```bash
python -m venv venv
source venv/bin/activate
pip install mailjet-rest
```

### From source

```bash
git clone https://github.com/mailjet/mailjet-apiv3-python
cd mailjet-apiv3-python
pip install .
```

On Unix platforms, `conda` and `make` are also supported:

```bash
make install
```

### For development

```bash
# minimal environment
make dev
conda activate mailjet

# full dev environment
make dev-full
conda activate mailjet-dev
```

The repository also ships a management script that wraps the common tasks:

```bash
./manage.sh env_setup     # conda environment and pre-commit hooks
./manage.sh test_all      # unit and integration tests
./manage.sh perf_bench    # performance profilers
./manage.sh format        # formatting
./manage.sh lint          # linting
```

## Authentication

```bash
export MJ_APIKEY_PUBLIC='your api key'
export MJ_APIKEY_PRIVATE='your api secret'
export MJ_CONTENT_TOKEN='your bearer token'   # optional, Content API v1
```

## Quick start

**Use the client as a context manager.** This pools and closes the underlying TCP connections for you and prevents resource leaks:

```python
import os
from mailjet_rest import Client

api_key = os.environ.get("MJ_APIKEY_PUBLIC", "")
api_secret = os.environ.get("MJ_APIKEY_PRIVATE", "")

with Client(auth=(api_key, api_secret), version="v3.1") as mailjet:
    data = {
        "Messages": [
            {
                "From": {"Email": "pilot@example.com", "Name": "Mailjet Pilot"},
                "To": [{"Email": "passenger1@example.com", "Name": "Passenger 1"}],
                "Subject": "Your email flight plan!",
                "TextPart": "Welcome to Mailjet! May the delivery force be with you!",
            }
        ]
    }
    result = mailjet.send.create(data=data)
    print(result.status_code)
```

**Warning:** if you do not use the context manager, you **must** call `mailjet.close()` on shutdown to release the sockets.

## Configuration

Overrides can be passed when the client is created, or per call:

```python
with Client(
    auth=(api_key, api_secret),
    version="v3.1",
    api_url="https://api.us.mailjet.com/",
    timeout=30,
) as mailjet:
    # override the timeout for a single heavy request
    result = mailjet.contact.get(timeout=60)
```

### API versioning

| Version | Scope |
|  --- | --- |
| `v3` | Email API |
| `v3.1` | Send API v3.1 (latest send version) |
| `v1` | Content API — templates, blocks, images |


Most Email API endpoints sit under `v3`, which is the default. For anything else, set `version`.

### Base URL

The default base domain is `api.mailjet.com`. Accounts on Mailjet's **US architecture** must set `https://api.us.mailjet.com`:

```python
with Client(auth=(api_key, api_secret), api_url="https://api.us.mailjet.com/") as mailjet:
    ...
```

## Building resource paths

Python identifiers cannot contain slashes or dashes, so paths are translated:

- replace a slash `/` with an underscore `_`
- replace a dash `-` by capitalising the next letter


`statistics/link-click` therefore becomes `statistics_linkClick`:

```python
with Client(auth=(api_key, api_secret)) as mailjet:
    filters = {"CampaignId": "xxxxxxx"}
    result = mailjet.statistics_linkClick.get(filters=filters)
    print(result.json())
```

For the **Content API (v1)**, sub-actions are routed with slashes (for example `contents/lock`), and `data_images` maps specifically to `/v1/data/images` to support media uploads.

## Error handling

Network-level exceptions are wrapped. Standard HTTP errors such as `404` or `400` **do not** raise — they return the `requests.Response` so you can inspect `status_code` and `.json()`:

```python
from mailjet_rest import CriticalApiError, TimeoutError, ApiError

try:
    result = mailjet.contact.get()
    if result.status_code != 200:
        print(f"API Error: {result.status_code} - {result.text}")

except TimeoutError:
    print("The request to the Mailjet API timed out.")
except CriticalApiError as e:
    print(f"Network connection failed: {e}")
```

## Logging and debugging

The SDK plugs into the standard `logging` library. If your payload carries identifiers such as `CustomID`, `Campaign` or `TemplateID`, they are extracted automatically and injected into the log line as a `Trace` context, so local errors can be correlated with your Mailjet dashboard analytics:

```python
import logging
from mailjet_rest import Client

logging.getLogger("mailjet_rest.client").setLevel(logging.DEBUG)
logging.basicConfig(format="%(levelname)s - %(message)s")

with Client(auth=(api_key, api_secret), version="v3.1") as mailjet:
    mailjet.send.create(
        data={
            "Messages": [
                {
                    "From": {"Email": "pilot@example.com"},
                    "To": [{"Email": "passenger@example.com"}],
                    "CustomID": "Promo_Black_Friday",
                }
            ]
        }
    )
```

Console output includes the trace: `DEBUG - Sending Request: POST ... | Trace: [CustomID=Promo_Black_Friday]`

### IDE autocompletion

The SDK dispatches URLs dynamically through `__getattr__`. To avoid accidentally dispatching internal Python methods as API requests, accessing private attributes or removed properties (such as `client.auth`) raises an explicit `AttributeError` instead of firing a ghost request.

Because of that dynamic dispatch, type checkers may report endpoints like `client.contact.create` as `Any`. Under strict typing you can safely ignore those specific calls.

## Typed payloads

### Strict payload builders

Import the TypedDict schemas for IDE autocomplete and static checking instead of debugging `400 Bad Request` responses caused by a typo:

```python
from mailjet_rest.types import SendV31Payload, SendV31Message

message: SendV31Message = {
    "From": {"Email": "pilot@example.com", "Name": "Mailjet Pilot"},
    "To": [{"Email": "passenger1@example.com", "Name": "Passenger 1"}],
    "Subject": "Your flight plan!",
    "TextPart": "Dear passenger, welcome to Mailjet!",
}

payload: SendV31Payload = {"Messages": [message]}

mailjet.send.create(data=payload)
```

### MessageBuilder and SendPayloadBuilder

For complex messages, the fluent builders handle structure, attachment encoding and validation:

```python
from mailjet_rest import Client
from mailjet_rest.builders import MessageBuilder, SendPayloadBuilder

message = (
    MessageBuilder()
    .set_sender("pilot@example.com", "Mailjet Pilot")
    .add_recipient("passenger@example.com", "John Doe")
    .add_cc("copilot@example.com")
    .set_subject("Your Boarding Pass")
    .set_content(html="<h3>Welcome aboard!</h3>")
    .attach_file("tickets/pass.pdf")     # encoded via a memory-efficient chunked streamer
    .attach_inline("assets/logo.png")
    .build()
)

payload = SendPayloadBuilder().add_message(message).set_sandbox_mode(True).build()

with Client(auth=(api_key, api_secret), version="v3.1") as mailjet:
    result = mailjet.send.create(data=payload)
    print(result.status_code)
```

## Send API examples

### Send a basic email

```python
data = {
    "Messages": [
        {
            "From": {"Email": "pilot@example.com", "Name": "Mailjet Pilot"},
            "To": [{"Email": "passenger1@example.com", "Name": "Passenger 1"}],
            "Subject": "Your email flight plan!",
            "TextPart": "Dear passenger 1, welcome to Mailjet!",
            "HTMLPart": "<h3>Dear passenger 1, welcome to Mailjet!</h3>",
        }
    ]
}

with Client(auth=(api_key, api_secret), version="v3.1") as mailjet:
    result = mailjet.send.create(data=data)
```

### Send with a Mailjet template

Pass `Variables` as a plain Python dictionary:

```python
data = {
    "Messages": [
        {
            "From": {"Email": "pilot@example.com", "Name": "Mailjet Pilot"},
            "To": [{"Email": "passenger1@example.com", "Name": "Passenger 1"}],
            "TemplateID": 1234567,
            "TemplateLanguage": True,
            "Subject": "Your email flight plan!",
            "Variables": {"name": "John Doe", "custom_data": "Welcome aboard!"},
        }
    ]
}

with Client(auth=(api_key, api_secret), version="v3.1") as mailjet:
    result = mailjet.send.create(data=data)
```

## REST request examples

The examples below assume an open session: `with Client(auth=(api_key, api_secret)) as mailjet:`

### POST — create an object

```python
data = {"Email": "passenger@example.com"}
result = mailjet.contact.create(data=data)
```

### POST — endpoints with an action

```python
data = {
    "ContactsLists": [
        {"ListID": list_id_1, "Action": "addnoforce"},
        {"ListID": list_id_2, "Action": "addforce"},
    ]
}
result = mailjet.contact_managecontactslists.create(id=contact_id, data=data)
```

### Sandbox mode for local development

`dry_run=True` intercepts every state-changing request (`POST`, `PUT`, `DELETE`) and returns a mock `200 OK`, so local runs cannot reach real recipients:

```python
with Client(auth=(api_key, api_secret), dry_run=True) as dry_run_client:
    dry_run_client.contact.create(data={"Email": "real_user@example.com"})
```

### GET — all objects

```python
result = mailjet.contact.get()
```

### GET — a single object

```python
result = mailjet.contact.get(id=contact_id)
```

### GET — with filters

```python
filters = {
    "limit": 40,
    "offset": 50,
    "sort": "Email desc",
    "IsExcludedFromCampaigns": "false",
}
result = mailjet.contact.get(filters=filters)
```

### Pagination

Many list endpoints accept `limit`, `offset` and `sort`:

| Parameter | Type | Meaning | Default |
|  --- | --- | --- | --- |
| `limit` | int | number of objects returned (max `1000`) | `10` |
| `offset` | int | starting position in the result list | `0` |
| `sort` | str | property plus `ASC` or `DESC`; not available for every property | `ID asc` |


#### Lazy pagination

`.stream()` returns a generator and manages limit, offset and network pagination for you — no `while` loop, and memory-safe on large lists:

```python
for contact in mailjet.contact.stream(chunk_size=500):
    print(contact["Email"])
```

### PUT — update an object

A `PUT` in the Mailjet API behaves like a `PATCH`: only the properties you send are updated, and non-mandatory properties can be omitted.

```python
data = {
    "Data": [
        {"Name": "first_name", "value": "John"},
        {"Name": "last_name", "value": "Smith"},
    ]
}
result = mailjet.contactdata.update(id=contact_id, data=data)
```

### DELETE — remove an object

A successful `DELETE` returns `204 No Content` with no response body.

```python
result = mailjet.template.delete(id=template_id)
```

## Email API ecosystem

### Webhooks

Subscribe to real-time events (open, click, bounce, …) through the `eventcallbackurl` resource:

```python
data = {
    "EventType": "open",
    "Url": "https://www.example.com/webhook",
    "Status": "alive",
}
result = mailjet.eventcallbackurl.create(data=data)
```

### Parse API

Route inbound email for a domain to your own webhook:

```python
data = {"Url": "https://www.example.com/mj_parse.php"}
result = mailjet.parseroute.create(data=data)
```

### Segmentation

Build expressions that filter contacts dynamically:

```python
data = {
    "Description": "Will send only to contacts under 35 years of age.",
    "Expression": "(age<35)",
    "Name": "Customers under 35",
}
result = mailjet.contactfilter.create(data=data)
```

### Statistics

```python
filters = {
    "CounterSource": "APIKey",
    "CounterTiming": "Message",
    "CounterResolution": "Lifetime",
}
result = mailjet.statcounters.get(filters=filters)
```

## Content API

The Content API (`v1`) manages templates, tokens and images. Set `version="v1"` and authenticate with Basic Auth or a bearer token. The SDK adds the required `/REST/` prefix for most resources automatically and maps `data_images` to `/data/`.

### Generate a token

```python
with Client(auth=(api_key, api_secret), version="v1") as client:
    data = {
        "Name": "My Access Token",
        "Permissions": ["read_template", "create_template"],
    }
    result = client.token.create(data=data)
```

### Upload an image

Use the `data_images` resource and remove the default `Content-Type` header so `requests` can generate the multipart boundaries:

```python
import base64

b64_string = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
image_bytes = base64.b64decode(b64_string)

files_payload = {
    "metadata": (None, '{"name": "logo.png", "Status": "open"}', "application/json"),
    "file": ("logo.png", image_bytes, "image/png"),
}

result = client.data_images.create(headers={"Content-Type": None}, files=files_payload)
```

### Lock template content

Sub-actions are routed with slashes — `template_contents_lock` becomes `template/{id}/contents/lock`:

```python
result = client.template_contents_lock.create(id=template_id)
```

## Security guardrails

The SDK ships active protections based on defence-in-depth principles:

- **SSRF and open redirects** — automatic redirects are disabled and hostname validation is enforced.
- **CRLF injection** — header injection through compromised bearer tokens or custom headers is blocked.
- **Downgrade attacks** — TLS 1.2 minimum is enforced via a custom `SecureHTTPAdapter`.
- **Idempotency fingerprinting** — SHA-256 `Idempotency-Key` headers are generated for `POST`, `PUT` and `DELETE` to prevent duplicate mutations.
- **SpamGuard analysis** — a pre-flight HTML analyser blocks XSS vectors (`<script>`, `onerror=`) before dispatch.
- **Secret obfuscation** — a custom `SecretAuth` transport adapter and redacting filter keep credentials out of tracebacks, logs and memory dumps.
- **Punycode IDN support** — internationalised domain names are normalised to prevent homograph attacks.


The vulnerability disclosure policy and supported versions are in [SECURITY.md](https://github.com/mailjet/mailjet-apiv3-python/blob/master/SECURITY.md).

### Local-first validation

Rather than waiting for a server round trip, strictly typed models (such as `SendV31Payload`) catch mass assignment and invalid formats locally in microseconds.

### Runtime security (PEP 578)

For SecOps environments the SDK acts as a sensor, emitting native `sys.audit` events for outbound network egress and explicit TLS bypass attempts. Opt in to pipe them into your logging infrastructure:

```python
from mailjet_rest import Client, Config

cfg = Config(enable_security_audit=True)

with Client(auth=(api_key, api_secret), config=cfg) as mailjet:
    ...
```

### Network resilience and retries

A `JitterRetry` connection-pooling policy handles transient drops, `5xx` errors and `429` rate limits using exponential backoff with randomised full jitter, which avoids a thundering herd on Mailjet's upstream during high-throughput batching.

Retries are configured out of the box, but you can mount your own adapter:

```python
import random
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from mailjet_rest import Client


class CustomJitterRetry(Retry):
    """Stateless retry policy with full jitter."""

    def get_backoff_time(self) -> float:
        backoff = super().get_backoff_time()
        return random.uniform(0, backoff) if backoff > 0 else 0


custom_retry_strategy = CustomJitterRetry(
    total=5,
    backoff_factor=0.5,
    status_forcelist=[429, 500, 502, 503, 504],
    raise_on_status=False,  # let the client raise Mailjet exceptions instead
)

with Client(auth=(api_key, api_secret)) as mailjet:
    mailjet.session.mount("https://", HTTPAdapter(max_retries=custom_retry_strategy))
    result = mailjet.contact.get()
```

## Performance

Since `v1.6.0` the SDK is optimised for high concurrency and memory-constrained environments such as AWS Lambda: `__slots__` for memory density, immutable `MappingProxyType` headers for zero-allocation merging, and O(1) dynamic endpoint caching. Benchmarks and profiling instructions are in the [performance guide](https://github.com/mailjet/mailjet-apiv3-python/blob/master/PERFORMANCE.md).

## Deprecation warnings

Legacy arguments (`ensure_ascii`, `data_encoding`), obsolete helpers (`parse_response`) and ambiguous routing (`v1` with `/template`) do not break your code. The request still executes, but a non-breaking `DeprecationWarning` is emitted so you can migrate gradually.

## Executable README

The repository ships a script that creates, exercises and cleans up all the resources used in the examples — a quick way to verify credentials and network access:

```bash
python samples/smoke_readme_runner.py
```

Further samples live in [`/samples`](https://github.com/mailjet/mailjet-apiv3-python/tree/master/samples).

## Contribute

The wrapper is open source and MIT-licensed. Fork the repository, branch, implement your fix or feature, document it, and open a pull request at [mailjet/mailjet-apiv3-python](https://github.com/mailjet/mailjet-apiv3-python). Documentation improvements belong in the [API documentation repo](https://github.com/mailjet/api-documentation).