Python SDK
The Python SDK is easiest to use with one mental model: create a temporary inbox, wait for the next email, then read the extracted field you need.
Python 3.9+ is required.
Install
pip install postmx
Super Simple Path
Async
import asyncio
from postmx import PostMX
async def main() -> None:
async with PostMX("pmx_live_...") as postmx:
inbox = await postmx.create_temporary_inbox({
"label": "signup-test",
})
print("Send your app email to:", inbox["email_address"])
message = await postmx.wait_for_message(
inbox["id"],
timeout=30.0,
)
print("OTP:", message["otp"])
print("First link:", message["links"][0]["url"] if message["links"] else None)
asyncio.run(main())
Sync
from postmx import PostMXSync
postmx = PostMXSync("pmx_live_...")
inbox = postmx.create_temporary_inbox({
"label": "signup-test",
})
message = postmx.wait_for_message(inbox["id"])
print(message["otp"])
wait_for_message() returns the latest existing message immediately if the inbox already has one; otherwise it waits for the next incoming email until the timeout.
What Do You Want Back?
The default message already includes extracted fields like otp, links, and intent.
If you already have a message ID, content_mode is just a simple "what do you want back?" choice:
otp_only = await client.get_message("msg_123", content_mode="otp")
print(otp_only["otp"])
links_only = await client.get_message("msg_123", content_mode="links")
print(links_only["links"])
Supported modes:
full: full message detailotp: message metadata plusotplinks: message metadata pluslinkstext_only: message metadata plustext_body
Create the client
Async client
from postmx import PostMX
client = PostMX("pmx_live_...")
Sync client
from postmx import PostMXSync
client = PostMXSync("pmx_live_...")
Constructor options
| Option | Type | Default | Notes |
|---|---|---|---|
base_url | str | https://api.postmx.co | Override for staged or local environments. |
max_retries | int | 2 | Retries apply to 429, 500, 502, 503, and 504. |
timeout | float | 30.0 | Request timeout in seconds. |
Advanced
Lifecycle controls
inbox = await client.create_inbox({
"label": "checkout-flow",
"lifecycle_mode": "temporary",
"ttl_minutes": 15,
})
ttl_minutes is optional for temporary inboxes. Current API limits are 10 to 60.
List inboxes
result = await client.list_inboxes(limit=20)
print(result["inboxes"])
print(result["page_info"])
print(result["wildcard_address"])
List messages for an inbox
result = await client.list_messages(inbox["id"], limit=10)
print(result["messages"])
List messages for a recipient email
result = await client.list_messages_by_recipient(
"[email protected]",
limit=10,
)
print(result["messages"])
Get a message
message = await client.get_message("msg_123")
print(message["subject"])
print(message["text_body"])
print(message["html_body"])
print(message["otp"])
print(message["links"])
Wait for a message
message = await client.wait_for_message(
inbox["id"],
interval=1.0,
timeout=60.0,
)
Notes:
intervaldefaults to1.0seconds.timeoutdefaults to60.0seconds.intervalmust be at least0.2seconds.wait_for_message()returns the latest existing message immediately if the inbox already has one; otherwise it waits for the next incoming email.
Create a webhook
result = await client.create_webhook({
"label": "production-events",
"target_url": "https://example.com/webhooks/postmx",
})
print(result["webhook"]["id"])
print(result["signing_secret"])
Use a final public HTTPS endpoint. The API rejects localhost targets, embedded credentials, and private or reserved IP literals. PostMX does not follow redirects, and each delivery attempt times out after 10 seconds.
Verify a webhook signature
from postmx import verify_webhook_signature
event = verify_webhook_signature(
payload=raw_body,
signature=request.headers.get("x-postmx-signature") or request.headers["postmx-signature"],
timestamp=request.headers["x-postmx-timestamp"],
signing_secret=signing_secret,
)
print(event["type"])
print(event["data"]["message"]["otp"])
Important:
- Store the returned
signing_secretimmediately. It is returned once. - Pass the raw body as
bytesorstr. - Read
X-PostMX-Delivery-Idfor attempt tracing andX-PostMX-Event-Idfor diagnostics. - The expected signature format is
v1=<base64url(hmac_sha256(signing_secret, timestamp + "." + raw_body))>. - The default timestamp tolerance is
300seconds. - The payload already includes the full message plus extracted fields such as
otp,links, andintent.
Error handling
from postmx import PostMXApiError, PostMXNetworkError
try:
await client.get_message("msg_missing")
except PostMXApiError as error:
print(error.status)
print(error.code)
print(error.request_id)
print(error.retry_after_seconds)
except PostMXNetworkError as error:
print(error)
Async vs sync
- Use
PostMXin async web apps, async workers, and async tests. - Use
PostMXSyncin scripts, sync apps, and basic automation. PostMXSynccannot be used inside an already running event loop. In async environments, usePostMXdirectly.- POST requests are safe to retry because the SDK auto-generates an
idempotency_keywhen you do not pass one.
Method reference
PostMX(api_key, *, base_url="https://api.postmx.co", max_retries=2, timeout=30.0)
await client.list_inboxes(*, limit=None, cursor=None)
await client.create_inbox(params, *, idempotency_key=None)
await client.create_temporary_inbox(params, *, idempotency_key=None)
await client.list_messages(inbox_id, *, limit=None, cursor=None)
await client.list_messages_by_recipient(recipient_email, *, limit=None, cursor=None)
await client.get_message(message_id, *, content_mode=None)
await client.create_webhook(params, *, idempotency_key=None)
await client.wait_for_message(inbox_id, *, interval=1.0, timeout=60.0)