Skip to main content

Mailjet V3 API wrapper

Project description

alt text

Official Mailjet Python Wrapper

PyPI Version GitHub Release Python Versions License PyPI Downloads Build Status GitHub Stars GitHub Issues GitHub PRs

Table of contents

Overview

Welcome to the Mailjet official Python API wrapper!

Check out all the resources and Python code examples in the official Mailjet Documentation.

Compatibility

This library mailjet-rest officially supports the following Python versions:

  • Python >= 3.10, < 3.15

Requirements

  • Build backend: setuptools, wheel, setuptools-scm
  • Runtime: requests >=2.33.0

Test dependencies

For running test you need pytest >=9.0.3 at least. Make sure to provide the environment variables from Authentication.

Installation

pip install

Create a virtual environment and install the wrapper:

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

git clone & pip install locally

Use the below code to install the wrapper locally by cloning this repository:

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

conda & make

Use the below code to install it locally by conda and make on Unix platforms:

make install

For development

Using conda

on Linux or macOS:

  • A basic environment with a minimum number of dependencies:
make dev
conda activate mailjet
  • A full dev environment:
make dev-full
conda activate mailjet-dev

Management script

We provide a universal management script (manage.sh) to simplify local development, testing, and linting.

# 1. Setup the conda environment and pre-commit hooks
./manage.sh env_setup
conda activate mailjet-dev

# 2. Run the test suite (Unit + Integration)
./manage.sh test_all

# 3. Run the performance profilers
./manage.sh perf_bench

# 4. Format and lint the code
./manage.sh format
./manage.sh lint

Authentication

The Mailjet Email API uses your API and Secret keys for authentication. Grab and save your Mailjet API credentials securely in your environment variables.

export MJ_APIKEY_PUBLIC='your api key'  # pragma: allowlist secret
export MJ_APIKEY_PRIVATE='your api secret'  # pragma: allowlist secret
export MJ_CONTENT_TOKEN='your_bearer_token' # Optional, for Content API v1

Quick Start

Best Practice: Use the Mailjet Client as a Context Manager (with statement) to automatically pool and close underlying TCP connections, preventing resource leaks.

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@mailjet.com", "Name": "Mailjet Pilot"},
                "To": [{"Email": "passenger1@mailjet.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] Resource Management: If you choose not to use the context manager, you must manually call mailjet.close() when your application shuts down to release the underlying sockets.

Advanced Configuration

You can pass configuration overrides directly when initializing the Client or during individual API calls:

# Set custom base URL, timeout, and API version
with Client(
    auth=(api_key, api_secret),
    version="v3.1",
    api_url="https://api.us.mailjet.com/",
    timeout=30,
) as mailjet:
    # Override timeout for a single, heavy request
    result = mailjet.contact.get(timeout=60)

API Versioning

The Mailjet API is spread among distinct versions:

  • v3 - The Email API
  • v3.1 - Email Send API v3.1, which is the latest version of our Send API
  • v1 - Content API (Templates, Blocks, Images)

Since most Email API endpoints are located under v3, it is set as the default one and does not need to be specified when making your request. For the others you need to specify the version using version. For example, if using Send API v3.1:

with Client(
    auth=(api_key, api_secret),
    version="v3.1",
) as mailjet:
    pass  # Your requests here

For additional information refer to our API Reference.

Base URL

The default base domain name for the Mailjet API is api.mailjet.com. You can modify this base URL by setting a value for api_url in your call:

with Client(
    auth=(api_key, api_secret),
    api_url="https://api.us.mailjet.com/",
) as mailjet:
    pass  # Your requests here

If your account has been moved to Mailjet's US architecture, the URL value you need to set is https://api.us.mailjet.com.

Usage

Error Handling

The client safely wraps network-level exceptions. Standard HTTP errors (like 404 Not Found or 400 Bad Request) do not raise exceptions; they return the requests.Response object directly so you can inspect status_code and .json().

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 & Debugging

The SDK integrates seamlessly with Python's standard logging library and features Smart Telemetry. If you pass identifiers like CustomID, Campaign, or TemplateID in your payload, the SDK automatically extracts them and injects a Trace context into your logs. This allows you to easily correlate local application errors with your Mailjet Dashboard analytics.

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:
    # Adding 'CustomID' enables Smart Tracing in the console logs
    mailjet.send.create(
        data={
            "Messages": [
                {
                    "From": {"Email": "test@test.com"},
                    "To": [{"Email": "user@test.com"}],
                    "CustomID": "Promo_Black_Friday",
                }
            ]
        }
    )

Console output will feature: DEBUG - Sending Request: POST ... | Trace: [CustomID=Promo_Black_Friday]

IDE Autocompletion & DX

Because the SDK utilizes dynamic URL dispatching (__getattr__), to prevent "Magic Method Traps" (accidentally dispatching internal Python methods), the SDK includes strict poka-yoke guardrails. Attempting to access private attributes or removed properties (like client.auth) will safely throw an explicit AttributeError instead of a ghost API request.

URL path

According to python special characters limitations we can't use slashes / and dashes - which is acceptable for URL path building. Instead, python client uses another way for path building. You should replace slashes / by underscore _ and dashes - by capitalizing next letter in path. For example, to reach statistics/link-click path you should call statistics_linkClick attribute of python client.

# GET `statistics/link-click`
with Client(auth=(api_key, api_secret)) as mailjet:
    filters = {"CampaignId": "xxxxxxx"}
    result = mailjet.statistics_linkClick.get(filters=filters)
    print(result.status_code)
    print(result.json())

For the Content API (v1), sub-actions will be correctly routed using slashes (e.g. contents/lock). Additionally, the SDK maps the data_images resource specifically to /v1/data/images to support media uploads.

# GET '/v1/data/images'
with Client(auth=(api_key, api_secret), version="v1") as mailjet:
    result = mailjet.data_images.get()

Strict Payload Builders

Tired of getting 400 Bad Request because of a typo in your JSON payload? Import our TypedDict schemas to get full IDE autocomplete and static type checking.

from mailjet_rest.types import SendV31Payload, SendV31Message

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

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

mailjet.send.create(data=payload)

Performance & Architecture

The Mailjet SDK v1.6.0+ has been heavily optimized for high-concurrency and memory-constrained environments (like AWS Lambda). It utilizes __slots__ for memory density, immutable MappingProxyType headers for zero-allocation merging, and O(1) dynamic endpoint caching.

For a detailed breakdown of our nanosecond routing benchmarks and instructions on how to profile the SDK, please read our Performance & Architecture Guide.

Security Guardrails

The SDK includes active protections against common API vulnerabilities based on Defense-in-Depth principles:

Additional built-in protections:

  • SSRF & Open Redirects: Hard-disabled automatic redirects and enforced strict hostname validation.
  • CRLF Injection: Native string evaluation blocks header injection attempts via compromised Bearer tokens or custom headers.
  • Downgrade Attacks: Enforced TLS 1.2+ minimum version via a custom SecureHTTPAdapter.

See our SECURITY.md for our vulnerability disclosure policy and supported versions.

Local-First Validation (Fail-Fast)

Instead of waiting for server-side roundtrips, the SDK promotes "Parse, Don't Validate" at the boundary. By using strictly typed models (like SendV31Payload), any attempt at Mass Assignment (BOPLA) or sending invalid data formats is caught locally in microseconds. (See the Strict Payload Builders section for examples).

Runtime Security (PEP 578)

For Enterprise and SecOps environments, the SDK acts as a security sensor. It emits native Python audit events (sys.audit) for all outbound network egress and explicit TLS bypass attempts.

You can opt-in to have the SDK automatically listen to these events and pipe them to your logging infrastructure for SIEM integration:

from mailjet_rest import Client, Config

# Activate the PEP 578 Audit Listener
cfg = Config(enable_security_audit=True)
with Client(auth=(api_key, api_secret), config=cfg) as mailjet:
    pass  # Your secure requests here

Request examples

Full list of supported endpoints

[!IMPORTANT]
This is a full list of supported endpoints this wrapper provides samples

Executable README (Smoke Test)

Want to test all these examples at once? We provide an executable script that dynamically creates, tests, and safely cleans up all resources mentioned in this document. It's a great way to verify your API credentials and network access.

Simply run:

python samples/smoke_readme_runner.py

Send API (v3.1)

Send a basic email

from mailjet_rest import Client
import os

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

data = {
    "Messages": [
        {
            "From": {"Email": "pilot@mailjet.com", "Name": "Mailjet Pilot"},
            "To": [{"Email": "passenger1@mailjet.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)
    print(result.status_code)
    print(result.json())

Send an email using a Mailjet Template

When using TemplateLanguage, ensure that you pass a standard Python dictionary to the Variables parameter.

data = {
    "Messages": [
        {
            "From": {"Email": "pilot@mailjet.com", "Name": "Mailjet Pilot"},
            "To": [{"Email": "passenger1@mailjet.com", "Name": "passenger 1"}],
            "TemplateID": 1234567,  # Put your actual Template ID here
            "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)

Building Complex Payloads (MessageBuilder)

For complex scenarios like Send API v3.1, manually constructing nested dictionaries is error-prone. The MessageBuilder provides a fluent interface that handles structure, attachment encoding, and validation automatically.

from mailjet_rest.builders import MessageBuilder
from mailjet_rest.types import SendV31Payload

# Fluently construct an email
message = (
    MessageBuilder()
    .set_sender("pilot@mailjet.com", "Mailjet Pilot")
    .add_recipient("passenger@mailjet.com", "John Doe")
    .add_cc("copilot@mailjet.com")
    .set_subject("Your Boarding Pass")
    .set_content(html="<h3>Welcome aboard!</h3>")
    .attach_file("tickets/pass.pdf")  # Automatically encodes and validates
    .build()
)

payload: SendV31Payload = {
    "Messages": [message],
    "SandboxMode": True,  # Remove to send a real message.
}
# Send via client
mailjet.send.create(data=payload)

Standard REST Actions (GET, POST, PUT, DELETE)

[!NOTE]
All examples in this section assume that you have already opened a session using the context manager, for example: with Client(auth=(api_key, api_secret)) as mailjet:.

POST (Create)

Simple POST request
# Create a new contact
data = {"Email": "Mister@mailjet.com"}
result = mailjet.contact.create(data=data)
print(result.json())
Using actions
# Manage the subscription status of a contact to multiple lists
id_ = "$ID"
data = {
    "ContactsLists": [
        {"ListID": "$ListID_1", "Action": "addnoforce"},
        {"ListID": "$ListID_2", "Action": "addforce"},
    ]
}
result = mailjet.contact_managecontactslists.create(id=id_, data=data)
print(result.json())
Zero-Leak Sandbox Mode (dry_run)

Developing locally? Stop accidentally sending emails to real users. Enable dry_run=True to safely intercept all network mutations (POST, PUT, DELETE).

# Intercepts state-changing requests and injects SandboxMode where applicable
with Client(auth=(api_key, api_secret), dry_run=True) as dry_run_client:
    # This will NOT hit the actual database, returning a mock 200 OK safely
    dry_run_client.contact.create(data={"Email": "real_user@example.com"})

GET Request

Retrieve all objects
# Retrieve all contacts
result = mailjet.contact.get()
print(result.json())
GET (Read one)
# Retrieve a specific contact ID
id_ = "Contact_ID"
result = mailjet.contact.get(id=id_)
print(result.json())
Using filtering
# Retrieve contacts that are not in the campaign exclusion list
filters = {
    "limit": 40,
    "offset": 50,
    "sort": "Email desc",
    "IsExcludedFromCampaigns": "false",
}
result = mailjet.contact.get(filters=filters)
print(result.json())
Using pagination

Some requests (for example GET /contact) has limit, offset and sort query string parameters. These parameters could be used for pagination. limit int Limit the response to a select number of returned objects. Default value: 10. Maximum value: 1000 offset int Retrieve a list of objects starting from a certain offset. Combine this query parameter with limit to retrieve a specific section of the list of objects. Default value: 0 sort str Sort the results by a property and select ascending (ASC) or descending (DESC) order. The default order is ascending. Keep in mind that this is not available for all properties. Default value: ID asc Next example returns 40 contacts starting from 51st record sorted by Email field descendally:

filters = {
    "limit": 40,
    "offset": 50,
    "sort": "Email desc",
}
result = mailjet.contact.get(filters=filters)
print(result.json())
Lazy Pagination (The .stream() method)

Stop writing while loops to fetch thousands of contacts. Use .stream() to return a native Python Generator. The SDK will automatically manage Limit, Offset, and network pagination under the hood.

# Fetch all contacts seamlessly. Memory-safe and clean.
for contact in mailjet.contact.stream(chunk_size=500):
    print(contact["Email"])

PUT (Update / Patch specific fields)

A PUT request in the Mailjet API will work as a PATCH request - the update will affect only the specified properties. The other properties of an existing resource will neither be modified, nor deleted. It also means that all non-mandatory properties can be omitted from your payload.

# Update the contact properties for a contact
id_ = "$CONTACT_ID"
data = {
    "Data": [
        {"Name": "first_name", "value": "John"},
        {"Name": "last_name", "value": "Smith"},
    ]
}
result = mailjet.contactdata.update(id=id_, data=data)
print(result.json())

DELETE (Returns 204 No Content)

Upon a successful DELETE request the response will not include a response body, but only a 204 No Content response code.

# Delete an email template
id_ = "Template_ID"
result = mailjet.template.delete(id=id_)
print(result.json())

Email API Ecosystem (Webhooks, Parse API, Segmentation, Stats)

Webhooks (Real-time Event Tracking)

You can subscribe to real-time events (open, click, bounce, etc.) by configuring a webhook URL using the eventcallbackurl resource.

data = {
    "EventType": "open",
    "Url": "https://www.mydomain.com/webhook",
    "Status": "alive",
}
result = client.eventcallbackurl.create(data=data)

Parse API (Receive Inbound Emails)

The Parse API routes incoming emails sent to a specific domain to your custom webhook.

data = {"Url": "https://www.mydomain.com/mj_parse.php"}
result = client.parseroute.create(data=data)

Segmentation (Contact Filters)

Create expressions to dynamically filter your contacts (e.g., customers under 35) using contactfilter.

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

Retrieve Campaign Statistics

Retrieve performance counters using statcounters or location-based statistics via geostatistics.

from mailjet_rest import Client
import os

auth = (
    os.environ.get("MJ_APIKEY_PUBLIC", ""),
    os.environ.get("MJ_APIKEY_PRIVATE", ""),
)
with Client(auth=auth) as mailjet:
    filters = {
        "CounterSource": "APIKey",
        "CounterTiming": "Message",
        "CounterResolution": "Lifetime",
    }
    # Getting general statistics
    result = mailjet.statcounters.get(filters=filters)
    print(result.status_code)
    print(result.json())

Content API

Requires version="v1". You can authenticate using Basic Auth or a Bearer Token.

The Content API (v1) allows managing templates, generating API tokens, and uploading images. The SDK handles the required /REST/ prefix for most resources automatically, while appropriately mapping data_images to /data/.

Generating a Token

# Tokens endpoint requires Basic Auth initially
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)
    print(result.json())

Uploading an Image via Multipart Form-Data

To upload physical files, use the data_images resource and delete the default Content-Type header so requests can generate proper multipart boundaries. The request will be mapped to /v1/data/images.

import base64

# Base64 encoded image data (1x1 transparent PNG)
b64_string = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
image_bytes = base64.b64decode(b64_string)

# The Image upload requires a JSON metadata part (with a Status) and the physical file part
files_payload = {
    "metadata": (None, '{"name": "logo.png", "Status": "open"}', "application/json"),
    "file": ("logo.png", image_bytes, "image/png"),
}

# Deleting the default Content-Type header allows requests to generate multipart/form-data
result = client.data_images.create(headers={"Content-Type": None}, files=files_payload)

Locking a Template Content

Sub-actions are safely handled using slashes (e.g., template_contents_lock becomes template/<template_id>/contents/lock).

template_id = 1234567

# This routes to POST /v1/REST/template/1234567/contents/lock
result = client.template_contents_lock.create(id=template_id)

Deprecation Warnings

The SDK includes an active native Python deprecation system to protect your application from sudden API breaking changes.

If you attempt to use legacy arguments (like ensure_ascii or data_encoding), obsolete utility functions (parse_response), or ambiguous routing (v1 with /template), the SDK will not break your code. It will successfully execute the request but will emit a non-breaking DeprecationWarning to help you gracefully migrate to modern standards.

Type Hinting

This SDK is fully type-hinted and compatible with static type checkers like mypy and pyright.

Because of the dynamic URL dispatch engine (__getattr__), IDEs may flag endpoints like client.contact.create as Any. If you enforce strict typing in your application, you may safely ignore these specific dynamically dispatched calls.

License

MIT

Contribute

Mailjet loves developers. You can be part of this project!

This wrapper is a great introduction to the open source world, check out the code!

Feel free to ask anything, and contribute:

  • Fork the project.
  • Create a new branch.
  • Implement your feature or bug fix.
  • Add documentation to it.
  • Commit, push, open a pull request and voilà.

If you have suggestions on how to improve the guides, please submit an issue in our Official API Documentation repo.

Contributors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mailjet_rest-1.7.0rc1.tar.gz (54.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mailjet_rest-1.7.0rc1-py3-none-any.whl (36.1 kB view details)

Uploaded Python 3

File details

Details for the file mailjet_rest-1.7.0rc1.tar.gz.

File metadata

  • Download URL: mailjet_rest-1.7.0rc1.tar.gz
  • Upload date:
  • Size: 54.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mailjet_rest-1.7.0rc1.tar.gz
Algorithm Hash digest
SHA256 c88baca527b6bbe7530f90dc8ec83fe7ffb07321c490eba8ad690d1dd0e0606e
MD5 955ac3c1a115f130d38379bcdc9dacd4
BLAKE2b-256 1225145ff594ce7e2a9119a76879c5c1b2458ccba30be9e1337e7022fb76fe89

See more details on using hashes here.

File details

Details for the file mailjet_rest-1.7.0rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for mailjet_rest-1.7.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 00962cdf50f6409cbbefc7db024bb57e84f4d97a67e3b5d141e0f79cb15f4f79
MD5 ea26715ddd54c713bf0ad1376f33acb2
BLAKE2b-256 7818ed4e4de69984f75ccb01fa9bbfe3a6ea1cd86c968a133c35fc8b673193f4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page