Skip to content
GuideRun end to end with Python and requests 2.32, 3 min read

Send and receive email in Python with requests

No SDK needed: Mailbay's REST API works with plain requests. This script receives and sends, and handles errors with the fix Mailbay returns.

Terminal
pip install requests
export MAILBAY_API_KEY=mb_live_xxxxxxxxx
python mailbay_demo.py
Terminal output of a Python script using requests: an inbox created, an email received with the code 590214 extracted, a message sent and delivered, and an error with its fix
The Python script from this page, run once: it created an inbox, waited for an email and got the code, sent a message and read its status, then hit an error that says how to fix it.

Set up in one paste

You need Python 3, the requests package and an API key from the dashboard. Nothing else: Mailbay has no Python SDK, and does not need one for this. If you have never made a key or an inbox, the quickstart walks through it in a minute.

python3 -m venv .venv && . .venv/bin/activate
pip install requests
export MAILBAY_API_KEY=mb_live_xxxxxxxxx

The script

Four steps: create an inbox, wait for an email and its code, send a message and read its status, then show what an error looks like. The call helper turns every Mailbay error into an exception that carries the fix, listed on the errors page.

import os
import requests

API = os.environ.get("MAILBAY_API_URL", "https://api.mailbay.dev/v1")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['MAILBAY_API_KEY']}"


def call(method, path, **kwargs):
    r = session.request(method, f"{API}{path}", timeout=90, **kwargs)
    if not r.ok:
        err = r.json()["error"]
        raise RuntimeError(f"{r.status_code} {err['code']}: {err['message']} (fix: {err['fix']})")
    return r.json() if r.content else None


# 1. Create an inbox
inbox = call("POST", "/inboxes", json={"local_part": "python-demo"})
print("inbox:", inbox["address"])

# 2. Block until a matching email arrives (up to 60 seconds)
found = call("GET", f"/inboxes/{inbox['id']}/wait", params={"subject": "code", "timeout": 60})
print("got:", found["message"]["subject"], "| codes:", found["codes"], "| links:", found["links"])

# 3. Send, then read the status back
sent = call("POST", f"/inboxes/{inbox['id']}/messages",
            json={"to": "delivered@test.mailbay.dev", "subject": "Hello from Python", "text": "It works."})
print("sent:", sent["id"], sent["status"])
print("now:", call("GET", f"/messages/{sent['id']}")["status"])

# 4. Errors carry a code and a fix
try:
    call("POST", "/inboxes", json={"local_part": "python-demo"})
except RuntimeError as e:
    print("error:", e)
  • GET /inboxes/{id}/wait

    Step 2 is a long poll

    One request that returns as soon as a message with “code” in the subject arrives, with the one time codes and links already extracted. Details under wait for an email.
  • delivered@test.mailbay.dev

    Step 3 sends to a test address

    It simulates delivery, so you can run the script before you verify a domain. bounced@ and complained@ work too. Before sending to real people from your own domain, send one message to the email deliverability test to check its authentication.

Run it

We ran the script against a local Mailbay stack. While it waited, a test service emailed a code to the new inbox; the wait returned it a few seconds later. The output is at the top of this page.

Retries without sending twice

A send that times out may still have gone through. Send an Idempotency-Key header, the same one on every retry of the same email, and Mailbay returns the first result instead of sending again. Keys are kept for 24 hours. We ran this twice in a row and got the same message id both times, with one email sent:

import os, uuid
import requests

API = os.environ.get("MAILBAY_API_URL", "https://api.mailbay.dev/v1")
headers = {"Authorization": f"Bearer {os.environ['MAILBAY_API_KEY']}"}
inbox_id = os.environ["INBOX_ID"]

key = str(uuid.uuid4())  # one key per logical email, reused on every retry
body = {"to": "delivered@test.mailbay.dev", "subject": "Receipt #1043", "text": "Thanks"}
for attempt in range(2):
    r = requests.post(f"{API}/inboxes/{inbox_id}/messages", json=body,
                      headers={**headers, "Idempotency-Key": key}, timeout=30)
    print(attempt, r.status_code, r.json()["id"])

Instead of waiting: webhooks

Waiting suits scripts, tests and agents. A web app should be told instead: register a webhook for message.received and the delivery events, and verify each request's signature with the Python function on the webhooks page. When a send bounces, the event carries the receiving server's reply, such as 550 5.1.1; the SMTP error code reference explains what each one means.

Coming from Resend's Python SDK? The Resend alternative page covers the migration. Testing a web app's emails? See the Playwright integration, and all integrations on the integrations page. Using Django or FastAPI? Their snippets are in the directory too.

Questions

Is there a Mailbay Python SDK?

Not today. The REST API is small enough to call with requests, as on this page, and every error comes back as JSON with a code, a message and a fix.

Can I send with smtplib instead?

Yes. Use smtp.mailbay.dev on port 587 with STARTTLS or 465 with TLS, the username mailbay and an API key that can send as the password. The sender must be on a domain verified in the project.

Why does my send fail with sandbox_recipient_not_allowed?

Until the workspace has a verified domain, inboxes send only to its members, its own addresses and the test addresses. Verify a domain to send to anyone.

How do I receive email in Python without waiting?

Register a webhook for message.received and handle it in Flask, FastAPI or Django. Mailbay signs each delivery, and the webhooks docs have a Python function that checks the signature.

Join the beta

Mailbay is in private beta and free while the beta runs: inboxes, sending, your own domains and MCP. We invite people in small batches and read every note on the form.