Skip to content

Getting Started

Greg Svoboda edited this page Jun 8, 2026 · 3 revisions

Getting Started

Prerequisites

Before you begin, you'll need:

  • A Postmark account
  • A Server API token (for sending email and server-level operations)
  • An Account API token (for account-level operations like managing domains and servers)
  • A verified sender signature or domain

You can find your tokens under API Tokens in the Postmark app.

Installation

pip install postmark-python

Python 3.10 or higher is required.

Quick Start

The library is fully async. All API calls must be awaited inside an async context.

Send Your First Email

import asyncio
import postmark

async def main():
    async with postmark.ServerClient("your-server-token") as client:
        response = await client.outbound.send({
            "sender": "you@verified-domain.com",
            "to": "recipient@example.com",
            "subject": "Hello from Postmark!",
            "text_body": "This is my first email via postmark-python.",
            "html_body": "<p>This is my first email via <strong>postmark-python</strong>.</p>",
        })
        print(f"Sent! Message ID: {response.message_id}")
        print(f"Accepted: {response.success}")  # True when error_code == 0

asyncio.run(main())

Note: You must use a verified sender address and a valid server token from your account.

Using the client as an async context manager (async with) is recommended. It ensures the underlying HTTP connection pool is cleanly closed when the block exits. For long-lived processes (servers, workers) you can also instantiate the client once and call await client.close() when shutting down.

Testing in Jupyter Notebooks? See the Jupyter Notebooks guide.

Sync / Scripting (no async required)

If you prefer a synchronous API — for one-off scripts, Flask apps, or just to avoid the async/await boilerplate — use postmark.sync:

import postmark

with postmark.sync.ServerClient("your-server-token") as client:
    response = client.outbound.send({
        "sender": "sender@example.com",
        "to": "recipient@example.com",
        "subject": "Hello from Postmark!",
        "text_body": "This is my first email via postmark-python.",
    })
    print(f"Message ID: {response.message_id}")

The sync client runs requests on a background event loop thread and blocks until each call completes. It supports all the same methods as the async client, and works correctly inside Jupyter notebooks.

For full documentation — both client types, streaming behaviour, context manager usage, and how it works under the hood — see the Sync Client page.

Two Client Types

Client Token Use for
ServerClient Server API token Sending email, bounces, templates, stats, webhooks, streams
AccountClient Account API token Domains, sender signatures, managing servers, data removals
import postmark

# Server-level operations
server_client = postmark.ServerClient("your-server-token")

# Account-level operations
account_client = postmark.AccountClient("your-account-token")

Using Environment Variables

Note on the examples: All example files in examples/async/ and examples/sync/ use hardcoded placeholder strings (e.g. "xxx-YOUR-SERVER-TOKEN-xxxx-xxxxxxx") in place of real credentials, to keep them as readable as possible. Replace those placeholders with your actual tokens before running — ideally by reading them from environment variables as shown below.

Store tokens in environment variables rather than hard-coding them in source files.

With python-dotenv (recommended for local development)

Create a .env file:

export POSTMARK_SERVER_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export POSTMARK_ACCOUNT_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export POSTMARK_SENDER_EMAIL=you@verified-domain.com

Then load it at the top of your script:

import os
import postmark
from dotenv import load_dotenv  # pip install python-dotenv

load_dotenv()

client = postmark.ServerClient(os.environ["POSTMARK_SERVER_TOKEN"])

Without python-dotenv

If you source your .env file in the shell before running (source .env), or if your deployment environment injects variables directly (Heroku, Railway, AWS Lambda, Docker, etc.), you can read them without the dotenv library:

import os
import postmark

client = postmark.ServerClient(os.environ["POSTMARK_SERVER_TOKEN"])

With postmark.sync

The same pattern applies to the sync client:

import os
import postmark

load_dotenv()  # if using python-dotenv

with postmark.sync.ServerClient(os.environ["POSTMARK_SERVER_TOKEN"]) as client:
    response = client.outbound.send({
        "sender": os.environ["POSTMARK_SENDER_EMAIL"],
        "to": "recipient@example.com",
        "subject": "Hello",
        "text_body": "Hello from Postmark!",
    })

Production environments

In production, inject secrets via your platform's secret management rather than .env files — environment variables are already available in the process without any extra loading:

Platform How to set
Heroku heroku config:set POSTMARK_SERVER_TOKEN=...
AWS Lambda Environment variables in function configuration
Docker --env flag or env_file in docker-compose.yml
GitHub Actions Repository secrets, referenced as ${{ secrets.POSTMARK_SERVER_TOKEN }}
Kubernetes Secret objects mounted as env vars

Dict vs. Model Input

Most methods accept either a plain dict or a typed Pydantic model. Using models gives you IDE autocompletion and catches errors before they reach the API.

from postmark import Email

# Using a dict
await client.outbound.send({
    "sender": "you@example.com",
    "to": "recipient@example.com",
    "subject": "Hello",
    "text_body": "Hello!",
})

# Using a model (recommended)
await client.outbound.send(
    Email(
        sender="you@example.com",
        to="recipient@example.com",
        subject="Hello",
        text_body="Hello!",
    )
)

Advanced Configuration

Custom Timeout

Default timeouts differ by client: ServerClient defaults to 5 seconds, AccountClient to 30 seconds. Override per instance:

client = postmark.ServerClient("your-server-token", timeout=60.0)

timeout must be a positive number — passing 0 or a negative value raises PostmarkException immediately at construction.

Custom Base URL

Override the API base URL — useful for pointing at a local mock server during tests:

client = postmark.ServerClient("your-server-token", base_url="http://localhost:8080")

Retry Behavior

The client automatically retries on RateLimitException, ServerException, and TimeoutException using exponential backoff with jitter. By default it retries up to 3 times. Adjust with the retries parameter:

# Retry up to 5 times before raising
client = postmark.ServerClient("your-server-token", retries=5)

# Disable retries entirely
client = postmark.ServerClient("your-server-token", retries=0)

retries must be a non-negative integer — passing a negative value raises PostmarkException immediately at construction.

Worst-case blocking time: With AccountClient defaults (timeout=30, retries=3), a single call that repeatedly times out can block for up to ~90 seconds before raising. ServerClient defaults (timeout=5, retries=3) cap this at ~15 seconds. Tune both values to match your application's latency budget.

SSL Verification

SSL verification can be disabled for development or testing. Do not disable in production.

import os
os.environ["POSTMARK_SSL_VERIFY"] = "false"

Debug Logging

import logging
logging.getLogger("postmark").setLevel(logging.DEBUG)

Next Steps

Clone this wiki locally