Ghost + Flask: Programmatically Adding New App Users into your Ghost Newsletter
6 min read

Ghost + Flask: Programmatically Adding New App Users into your Ghost Newsletter

I also like to be a little bit lazy about repetitive things, so if I can programmatically take care of something, I'll spent a day fiddling around to make it work.

xkcd: https://xkcd.com/1319/

I like to use Ghost as my CMS/newsletter and it's worked quite well for me so far. I also run Recommended Systems as a Python/Flask app and figured, "can I use Ghost as both my company's blog and as my customer newsletter software?" (The short answer is yes.)

So now in addition to using Ghost for public blogging, I can also use it for sending things like platform updates and release notes to a smaller subset of user that I identify as my customers.

This walkthrough will show you how to set up a similar function in your Flask app to make sure that when a new user signs up to the app, they also get signed up for customer-specific release notes emails that you can send through Ghost.


Step 0: Flask & Ghost

I'm going to go ahead and say that for this to work, you need to have (a) a working Flask app and (b) a working and properly configured Ghost installation. This walkthrough also assumes you understand what Flask Routes are and how they work.

(If you're not there yet, go ahead and set that up first.)


Step 1: Ghost Config

The Ghost API Key

Once you have Ghost installed (and both types of email sending configured), log in to your admin console, go setting, then to the bottom of settings and select "Integrations". Skip the "Built-In" stuff and scroll to the bottom to add a "Custom Integration".

I named mine "Flask Member Connector" but feel free to call yours whatever you'd like.

Once you've done that, you Ghost will generate an Admin API Key (it's the extra long one). You can access it again later, but copy it down for reference for now.

Ghost Member Labels

I use Ghost's labels to segment my audience, and specifically a customer label to segment general blog subscribers from my app customers. If you have a large list of members or already use a specific tag, make a note of that. It will be important for later.


Step 2: Flask Configuration

Generating the Ghost Auth JWT Token

Ghost requires token-based authentication and helpfully offers some off-the-shelf libraries for generating JWT tokens in Python. The only requirements are jwt, requests, and datetime:

import requests # pip install requests
import jwt	# pip install pyjwt
from datetime import datetime as date

# Admin API key goes here
key = 'YOUR_ADMIN_API_KEY'

# Split the key into ID and SECRET
id, secret = key.split(':')

# Prepare header and payload
iat = int(date.now().timestamp())

header = {'alg': 'HS256', 'typ': 'JWT', 'kid': id}
payload = {
    'iat': iat,
    'exp': iat + 5 * 60,
    'aud': '/admin/'
}

# Create the token (including decoding secret)
token = jwt.encode(payload, bytes.fromhex(secret), algorithm='HS256', headers=header)

# Make an authenticated request to create a post
url = 'http://localhost:2368/ghost/api/admin/posts/'
headers = {'Authorization': 'Ghost {}'.format(token)}
body = {'posts': [{'title': 'Hello World'}]}
r = requests.post(url, json=body, headers=headers)

print(r)

Note that this requires pip adding PyJWT (and not jwt). If you run into an issue where module 'jwt' has no option 'encode', then you've probably mixed the two packages up at some point. To fix that, uninstall both packages and reinstall the correct one:

# uninstall jwt and PyJWT
pip3 uninstall jwt
pip3 uninstall PyJWT

# install PyJWT
pip3 install PyJWT

Writing the Flask Function

In your Project's app directory, create a new functions.py file. I recommend writing this out as a separate function for testability, and pulling your API keys into .env variables so that they're not accidentally passed into Git and exposed to the world. But it's your call: if you want to write this all inline in a route, that's your call.

I made some slight modifications to the token script Ghost provided, explained below:

import os
import json
import requests
import datetime
from datetime import datetime, timedelta
import jwt

from dotenv import load_dotenv

# Set a path for pulling secrets from your .env file
basedir = os.path.abspath(os.path.dirname(__file__))
dotenv_path = os.path.join(basedir, ".env")
load_dotenv(dotenv_path)

"""
This script programmatically adds a user to Ghost as a "customer" label when they subscribe.
Token generation is pulled directly from the Ghost API: https://ghost.org/docs/admin-api/#members
"""

def update_ghost(name, email):
    key = os.environ.get("GHOST_KEY") # Admin API key goes here
    # Split the key into ID and SECRET
    id, secret = key.split(':')
    
    # Prepare header and payload
    iat = int(datetime.now().timestamp())
    
    header = {'alg': 'HS256', 'typ': 'JWT', 'kid': id}
    payload = {
        'iat': iat,
        'exp': iat + 5 * 60,
        'aud': '/admin/'
    }
    
    # Create the token (including decoding secret)
    token = jwt.encode(payload, bytes.fromhex(secret), algorithm='HS256', headers=header)
    
    # Make an authenticated request to create a post
    url_base = "https://[YOUR GHOST URL]"
    url_api = "/ghost/api/admin/members/"
    url = url_base + url_api # This is the endpoint URL for creating members.
    headers = {'Authorization': 'Ghost {}'.format(token)} 
    body = {
        "members": [
            {
                "name": name,
                "email": email,
                "labels": [
                    {
                        "name": "Customer",
                        "slug": "customer"
                    }
                ]
            }
        ]
    }
    
    r = requests.post(url, json=body, headers=headers)
    
    print("Ghost Function:")
    print(r)
    print("----------------")
    # return jsonify({"update": str(r)}) # You can use this for testing. 201 is the response code you're looking for.

The first change is importing os so that you can reference variables in your .env file, and referencing it as GHOST_KEY . Go ahead and create that now in your .env.

Next, you should update the inline url variables (line 39) to reflect where your ghost admin is located and which endpoint you're calling. (Don't add a trailing /). I separated this out to make it easier to switch when you hit different APIs, though that's outside the scope of this walkthrough.

If you want to explore the full extent of Ghost's APIs, they are available here: https://ghost.org/docs/admin-api/#endpoints

The POST Body

Line 43 starts the post body:

body = {
        "members": [
            {
                "name": name,
                "email": email,
                "labels": [
                    {
                        "name": "Customer",
                        "slug": "customer"
                    }
                ]
            }
        ]
    }

This is what you're passing along to Ghost and is pretty straightforward. The minimum required data for Ghost to create a new member is an email address. Because I capture the name of the user, I pass that as well (Ghost accepts a single concatenated name field).

Lastly, I label all of the folks I add programmatically as customer so that I can segment them out when I send emails. You select this when choosing who to publish the email to. For example:

Call this function from your Flask Route

You're almost done!

Our goal for this connector is to create a new Ghost member whenever anyone signs up for our app. So you need to figure out where in your app you want to trigger this function.

I choose to do it after someone signs up and verifies their account by email (to avoid accidently spamming people).

Once you're found the route you're working with, add in the function before your return call. Because we defined the function above as def update_ghost(name, email) we need to remember to do two things:

  1. Make sure to include it at the top of your Routes file with from app.functions import update_ghost (or wherever you created the file).
  2. Create and pass in the name and email variables when you call update_ghost(name, email).

Because I call this when I verify my users, I already have a user object open and can pass the user.name and user.email fields.

Step 3: Run It!

Now run the app. When you go through your flow, you should trigger the function and create a new member in Ghost with the label "customer":

Because we also added in a print() statement at the end of our function, your terminal should show the results of your POST call as well. Assuming all goes well, you'll see it tell you <Response [201]>, which means that as one or more new resources have been successfully created on the server.

And that's all there is to it!


Thank you for reading.

Was this useful? Interesting? Have something to add? Let me know. Seriously, I love getting email and hearing from readers. Shoot me a note at hi@romandesign.co with your thoughts and I promise I'll respond.


If you found this interesting, you can sign up for updates when there's a new post. It's really easy:

Thanks,
Roman