Skip to content

Python Guide

If you haven't already, see Getting Started. The following examples will assume you have already received your API key and created a namespace.

mailisk-python

Mailisk offers a Python library mailisk-python. This is an official typed client that wraps the API Reference endpoints into Python methods.

See the README for more usage examples.

Installation

Install the library with pip:

shell
pip install mailisk

The package requires Python 3.9 or newer.

Setup Client

Once installed, import the library and create the client by passing in your API key.

python
from mailisk import MailiskClient

mailisk = MailiskClient(api_key="YOUR_API_KEY")

The library does not load .env files or environment variables by itself. Load configuration however your application prefers, then pass the API key to the client.

python
import os

from mailisk import MailiskClient

mailisk = MailiskClient(api_key=os.environ["MAILISK_API_KEY"])

Reading Email

You can read emails using the search_inbox method.

  • By default it uses the wait flag. This means the call won't return until at least one email is received. Disabling this flag via wait: False can cause it to return an empty response immediately.
  • The request timeout is adjustable by passing timeout in the request options. By default it uses a timeout of 5 minutes. Timeouts are in seconds.
  • By default it returns emails in the last 15 minutes. This ensures that only new emails are returned. Without this, older emails would also be returned, potentially disrupting you if you were waiting for a specific email. This can be overridden by passing the from_timestamp parameter (from_timestamp: 0 will disable filtering by email age).
python
# timeout of 5 minutes
mailisk.search_inbox(namespace)

# timeout of 1 minute
mailisk.search_inbox(namespace, {}, {"timeout": 60})

# returns immediately, even if the result would be empty
mailisk.search_inbox(namespace, {"wait": False})

Filter arguments align with the Search Inbox API reference: to_addr_prefix, from_addr_includes, subject_includes, from_timestamp, to_timestamp, limit, offset, and wait.

python
response = mailisk.search_inbox(
    namespace,
    {
        "to_addr_prefix": f"john@{namespace}.mailisk.net",
        "subject_includes": "password",
    },
)

emails = response["data"]

The response will look similar to this:

json
{
  "total_count": 1,
  "options": {
    "limit": 10,
    "offset": 0
  },
  "data": [
    {
      "id": "1659368409795-42UcuQtMy",
      "from": {
        "address": "contact@mailisk.com",
        "name": ""
      },
      "to": [
        {
          "address": "john@sh7o3t3e4jvj.mailisk.net",
          "name": ""
        }
      ],
      "subject": "Test - Welcome to Mailisk",
      "html": "<html>...",
      "text": "*Welcome to Mailisk*\n\nHello there\nWelcome to Mailisk! We're excited to have you!\n\nGo ahead and ...",
      "received_date": "2022-08-01T15:40:09.000Z",
      "received_timestamp": 1659368409,
      "expires_timestamp": 1659372009
    }
  ]
}

The total_count tells us the total number of emails that match our query. Depending on limit and offset, a subset of this will be returned. See the Search Inbox API reference for more information on the other fields.

Sending Email

Outbound email

Use send_email when you want Mailisk to create and queue an outbound message through the API. Recipients must be verified external destinations or addresses under the same namespace. Messages sent only to the same namespace do not consume outbound usage because they are counted as inbound email when delivered.

python
namespace = "mynamespace"

outbound_email = mailisk.send_email(
    namespace,
    {
        "from": {
            "email": f"support@{namespace}.mailisk.net",
            "name": "Support",
        },
        "to": [f"customer@{namespace}.mailisk.net"],
        "subject": "Hello from Mailisk",
        "text": "This is the plain text body.",
        "html": "<p>This is the HTML body.</p>",
    },
)

print(outbound_email["id"], outbound_email["status"])

Fetch delivery details with get_outbound_email.

python
detail = mailisk.get_outbound_email(outbound_email["id"])

print(detail["delivery_summary"])
print(detail["recipients"])

You can send attachments by base64-encoding the file content. Use application/octet-stream when you need the file to remain a downloadable attachment across mail clients and parsers.

python
import base64
from pathlib import Path

content = Path("report.txt").read_bytes()

mailisk.send_email(
    namespace,
    {
        "to": [f"customer@{namespace}.mailisk.net"],
        "subject": "Report",
        "text": "Attached.",
        "attachments": [
            {
                "filename": "report.txt",
                "content_type": "application/octet-stream",
                "content_base64": base64.b64encode(content).decode("ascii"),
            }
        ],
    },
)

To verify the attachment arrived, search the inbox and download the attachment by ID.

python
response = mailisk.search_inbox(
    namespace,
    {
        "subject_includes": "Report",
        "to_addr_prefix": f"customer@{namespace}.mailisk.net",
    },
)

attachment = response["data"][0]["attachments"][0]
downloaded = mailisk.download_attachment(attachment["id"])

if downloaded != content:
    raise RuntimeError("Attachment content did not match")

Reply and forward

Use reply_to_email to send a reply to an inbound message.

python
reply = mailisk.reply_to_email(
    emails[0]["id"],
    {
        "subject": "Re: Thanks",
        "text": "We received your message.",
    },
)

print(reply["id"], reply["status"])

Use forward_email to forward an inbound message to another recipient.

python
forwarded = mailisk.forward_email(
    emails[0]["id"],
    {
        "to": ["verified@example.com"],
        "subject": "Fwd: Support request",
        "text": "Forwarding this along.",
    },
)

print(forwarded["id"], forwarded["status"])

Request SMS Numbers

Request and activate an SMS number from the dashboard (see SMS Testing). Surface it to your Python tests via configuration, e.g. os.environ["MAILISK_SMS_NUMBER"], so your specs all hit the same destination.

Listing SMS Numbers

Surface the list of approved numbers tied to your API key with list_sms_numbers(). This is useful for one-time setup scripts or sanity checks in CI before running OTP flows.

python
sms_numbers = mailisk.list_sms_numbers()["data"]
if not sms_numbers:
    raise RuntimeError("No SMS numbers available")

active_number = next((num for num in sms_numbers if num["status"] == "active"), None)
if active_number is None:
    raise RuntimeError("Activate an SMS number before running tests")

print(f"Using SMS number {active_number['phone_number']}")

Reading SMS

Use search_sms_messages(phone_number, params=None, request_options=None) to poll for inbound texts. It follows the same defaults as search_inbox:

  • wait defaults to True, holding the request open until at least one SMS matches the filters.
  • The third request_options argument lets you override the 5 minute timeout to handle slower OTP flows. Timeouts are in seconds.
  • Filter arguments align with the Search SMS API: body, from_number, from_date, to_date, limit, offset, etc. Date filters such as from_date and to_date should be ISO timestamp strings.
python
import os
import re

sms_number = os.environ["MAILISK_SMS_NUMBER"]
response = mailisk.search_sms_messages(
    sms_number,
    {
        "body": "Your login code",
    },
    {
        "timeout": 120,
    },
)

if not response["data"]:
    raise RuntimeError("SMS not received in time")

sms = response["data"][0]
match = re.search(r"(\d{6})", sms["body"])
if match is None:
    raise RuntimeError("SMS did not contain a 6 digit code")

otp = match.group(1)

If you need to reuse the same number across test runs, store run-specific state in your application (user IDs, tenants, etc.) and filter using the message body. Alternatively, avoid running parallel tests that share the same number, and set from_date to an ISO timestamp string to exclude messages created before the current test run.

An example with test Playwright is available here.

Sending Virtual SMS

To simulate inbound SMS (useful for previewing one-off notifications or driving tests without your production provider), call send_virtual_sms. Provide both numbers in E.164 or local dial format; Mailisk will deliver the SMS into the destination number you own.

python
import os

mailisk.send_virtual_sms(
    {
        "from_number": "+15551234567",
        "to_number": os.environ["MAILISK_SMS_NUMBER"],
        "body": "Your login code is 123456",
    }
)

Virtual SMS do not consume your regular inbound quota and land in the same search results as provider-delivered messages, so you can keep using search_sms_messages to assert content.

Authenticator TOTP

The Python client can generate authenticator app one-time passwords and manage saved virtual TOTP devices. The SDK calls the Mailisk API for code generation and device storage; it does not generate TOTP codes locally.

Configure the client with your Mailisk API key before using TOTP methods.

python
import os

from mailisk import MailiskClient

mailisk = MailiskClient(api_key=os.environ["MAILISK_API_KEY"])

Use the flow that matches your test setup:

Use caseMethod
Generate a current code from a known Base32 secret without saving anythingget_totp_otp_by_shared_secret(secret)
Save a reusable device from a Base32 shared secret with default settingscreate_totp_device(params)
Save a reusable device with custom TOTP settingscreate_custom_totp_device(params)
Save a device from a Base32 secret-key fieldcreate_totp_device_from_base32_secret_key(params)
Save a device from an otpauth://totp/... setup URLcreate_totp_device_from_otp_auth_url(params)
Generate a code for a saved deviceget_totp_otp_by_device_id(device_id)
List or delete saved deviceslist_totp_devices(params=None), delete_totp_device(device_id)

Generate a code without saving a device

Use this when your test already has the shared secret and does not need saved-device management.

python
otp = mailisk.get_totp_otp_by_shared_secret("JBSWY3DPEHPK3PXP")

page.fill("[name='otp']", otp["code"])
print(f"Code expires at {otp['expires']}")

This method uses the default TOTP settings: 6 digits, a 30 second period, and SHA1.

Save a device and reuse it later

Use create_totp_device for a saved device with the default TOTP settings.

python
device = mailisk.create_totp_device(
    {
        "name": "GitHub staging",
        "shared_secret": "JBSWY3DPEHPK3PXP",
        "expires_at": "2026-06-01T12:00:00.000Z",
    }
)

otp = mailisk.get_totp_otp_by_device_id(device["id"])

page.fill("[name='otp']", otp["code"])

Use create_custom_totp_device when the application under test uses non-default settings or when you want issuer and username metadata on the saved device.

python
device = mailisk.create_custom_totp_device(
    {
        "name": "GitHub staging",
        "secret": "JBSWY3DPEHPK3PXP",
        "username": "qa@example.com",
        "issuer": "GitHub",
        "digits": 6,
        "period": 30,
        "algorithm": "SHA1",
    }
)

otp = mailisk.get_totp_otp_by_device_id(device["id"])

Create a device from a Base32 secret key

Some test setup flows expose the shared secret as a Base32 secret key. Use create_totp_device_from_base32_secret_key when that field name matches your integration.

python
device = mailisk.create_totp_device_from_base32_secret_key(
    {
        "base32_secret_key": "JBSWY3DPEHPK3PXP",
        "username": "qa@example.com",
        "issuer": "GitHub",
    }
)

Create a device from an otpauth URL

Use create_totp_device_from_otp_auth_url when your application exposes the authenticator setup URL directly or through a QR-code payload.

python
device = mailisk.create_totp_device_from_otp_auth_url(
    {
        "otp_auth_url": "otpauth://totp/GitHub:qa@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub",
        "name": "GitHub staging",
    }
)

otp = mailisk.get_totp_otp_by_device_id(device["id"])

The URL must include a secret query parameter. Values inside the URL are used for fields such as issuer, username, digits, period, and algorithm when present.

Avoid near-expired codes

Pass min_seconds_until_expire when your test needs a code with enough validity left for form submission. If the current code has fewer seconds remaining, Mailisk waits for the next TOTP period before returning.

python
otp = mailisk.get_totp_otp_by_shared_secret(
    "JBSWY3DPEHPK3PXP",
    {"min_seconds_until_expire": 10},
)

For saved devices, pass the same option when requesting the code by device ID:

python
otp = mailisk.get_totp_otp_by_device_id(
    device["id"],
    {"min_seconds_until_expire": 10},
)

The value defaults to 0, which returns the current code immediately. It must be lower than the TOTP period.

List and delete saved devices

list_totp_devices returns active, non-expired devices for the organisation. issuer and username filters are case-insensitive partial matches.

python
result = mailisk.list_totp_devices(
    {
        "limit": 20,
        "offset": 0,
        "issuer": "GitHub",
        "username": "qa@example.com",
    }
)

print(result["total_count"])
print(result["items"])

Delete saved devices when they are no longer needed.

python
mailisk.delete_totp_device(device["id"])

Successful deletes return no response body.

Supported TOTP settings

Default settings:

SettingDefault
digits6
period30 seconds
algorithmSHA1

Supported custom values:

FieldAccepted values
digits6 or 8
periodInteger from 10 to 300 seconds
algorithmSHA1, SHA256, or SHA512

Secrets are write-only. The API accepts shared secrets when creating devices or generating codes, but saved device responses do not include the secret.

Response shapes

Saved device responses use this shape:

python
from typing import Optional

from typing_extensions import Literal, TypedDict


class TotpDevice(TypedDict):
    id: str
    organisation_id: str
    name: str
    username: Optional[str]
    issuer: Optional[str]
    digits: int
    period: int
    algorithm: Literal["SHA1", "SHA256", "SHA512"]
    source: str
    expires_at: Optional[str]
    created_at: str
    updated_at: str

OTP responses include the current code and an ISO timestamp for when that code expires:

python
class TotpOtpResponse(TypedDict):
    code: str
    expires: str

For REST request examples and endpoint-level errors, see the Authenticator TOTP API reference.

Typing

The package includes py.typed and exports TypedDict/Literal types for API parameters and responses.

python
from mailisk import MailiskClient, SearchInboxResponse

client = MailiskClient(api_key="YOUR_API_KEY")
response: SearchInboxResponse = client.search_inbox("mynamespace")

You can import request and response types from mailisk directly, for example SendEmailParams, SearchInboxResponse, OutboundEmailResponse, OutboundEmailDetailResponse, and TotpOtpResponse.