How to send and track the status of email using Python and the Postmark API

I've used the Postmark API in many projects, for both inbound and outbound email. Given the complexity of many cloud-based services these days, it's great to be able to hand off the email part to a dedicated provider.

Like many cloud developers, I have been creating several new apps using Python. The beauty of Python is in its visual simplicity and the many powerful libraries that abstract essential functions like JSON encoding and sending HTTP requests. Postmark's API allows us to send emails with very few lines of code, and it also allows us to track the status of those email operations. In this blog post, I'll be showing the steps to take to get that going.

Authentication #

Postmark allows you to create one or more authorization tokens that can be associated with different apps. This is an approach that is easy to work with, and it allows you to revoke a token at any time if it will no longer be used or has been compromised. To create a token, select the server in your Postmark account, and under credentials, press the button 'Generate another token' to create tokens that you can use in your app. This token has to be added as a header in the REST request to the server (typically a GET or POST).

In Python it looks like this:

headers = { 'Content-Type': 'application/json', 'X-Postmark-Server-Token': 'b9654a7f-8ed5-4d13-af41-f77e95d3d055', 'Accept':'application/json'}

This Python dictionary contains any HTTP headers needed by the request, here specifying the token (as required by the Postmark API) and the content-type, which, as we'll see in a moment, tells Postmark how to interpret the data body we are about to send.

Building the request #

To send an email via Postmark, we have to send a REST 'POST' with a body containing the details of the email request. At a minimum, we need to specify who the recipient is ('To') and to provide an email message body ('HtmlBody'), but of course it's also conventional to provide the sender email ('From') and an email 'Subject'. Again, when using Python, it's typical to use a dictionary:

parameters = {'From': 'martyn@postpush.info', 'To': 'recipient@postpush.info', 'Subject': 'Poem', 'HtmlBody': '<b>The boy stood on the burning deck</b>'}

Note that the 'From' field cannot just specify any random address that the Python program wants. In the Postmark console, each authorized sender address (or sender domain) needs to be set-up in the Postmark console under 'Sender Signatures'. In this way, Postmark can ensure that the platform cannot be subverted and used to generate spam emails.

Earlier, we specified the content-type as JSON. We need to make sure that this is how Python will send it, so we use the 'json' library to encode it:

import json
data = json.dumps(parameters)

And now we are ready to send the data using the Python 'requests' library:

import requests
r = requests.post('https://api.postmarkapp.com/email', headers=headers, data=data )

This sends a POST with our JSON body and the authentication key that we have set up. There is only one more thing to do, which is to get the response from the Postmark API (which is coded in JSON) and extract certain interesting information from there, notably the ErrorCode that tells us that the send to Postmark is successful, and the MessageID, which is the unique ID of that email in the Postmark system:

response = json.loads(r.text)
if response['ErrorCode'] == 0:
   	print('Message ID = %s' % response['MessageID'])
else:
   	print('Message not sent')

The MessageID is useful because we can use it to programmatically check what happened after we queued our message to be sent. We can also use this ID when looking at the Activity logs in the Postmark console, which is an excellent tool to help reconstruct 'what happened' for support purposes when something goes wrong on a live system.

Checking for status #

So far, we have queued an email to be sent by Postmark, but of course, for an email, this is only the start. Once an email is sent, it may be rejected for security purposes, or because the email address is no longer active, or for other different reasons. Therefore, we need another API that reads back the status of a queued email, which we can do using the MessageID that we got when we first sent the message:

messageID = 'f5f69627-xxxx-xxxx-xxxx-xxxxxxxxxxx'
r = requests.get('https://api.postmarkapp.com/messages/outbound/%s/details' % messageID, headers=headers)

Here, 'headers' will be the same as the earlier example, with the 'Accept' parameter ensuring that the answer that we get back from this GET will be in JSON so that we can easily handle it. We can then read the response back, convert from JSON back to dictionary format, which allows us to access interesting fields (like 'Status' and 'MessageEvents') efficiently.

response = json.loads(r.text)
status = response['Status']
if status == 'Sent':
   	for event in response['MessageEvents']:
          	print(event['Type']) 	# 'Delivered', 'Bounced'
          	print(event['ReceivedAt'])
   	
else:
   	print('Status = %s' % status)

Assuming that the status is 'Sent', we can expect that in time that the email will either be successfully delivered or will bounce. Some receiving email servers make several attempts to get an email to a user, so it can be minutes or hours before Postmark can definitively report that an email has been rejected. Here, we're showing the way that you can poll the Postmark API for an updated status on a send. You can also setup Postmark with a 'webhook' to call a public URL in your system when the status changes, but that is a topic for another day.

In the example code above, we iterate over all the entries in MessageEvents (which is an array), and this allows us to see any historical events for our message until we see that the message has definitively either been delivered successfully or bounced. We also get a 'ReceivedAt' timestamp, which may be useful to write to a log or database in our app, so that in a live environment when we get a failure, we have both MessageID and timestamp that allows us to map events in our app to events in the Postmark logs. In my experience, this has proved to be very valuable in providing the level of confidence that customers want when they ask for an explanation of what happened with their application-generated emails.

Finally #

I've posted the complete code on my Github page. Take it, experiment with it, and have fun with it.

Martyn Davies

Martyn Davies

Martyn Davies is a software engineer based in London, UK, working on cloud platforms in the telecom industry.

Interested in growing your audience? Write for the Postmark blog

Join 28,000 subscribers and get monthly tips, product announcements, and expert interviews.