Getting members into Ghost

Moving a list or building an integration? Here are some options!

Getting members into Ghost
💡
Welcome, readers! First off, if you haven't spent a lot of time thinking about email lists and how to keep them clean and deliverable, you should probably read my diatribe about how important opt-in for email is. There's some important context there that you should have before moving on to using one of the options here.

Testing was done on Ghost Pro, version 5.93.0.

Preliminary remark

If all you want to do is import a spreadsheet of members into Ghost, this is perhaps way more post than you need. There are good directions for doing that over in the Ghost documentation. The rest of this post is about getting members into Ghost through automated means. Want to make accounts for people who donate to your site? Want to make accounts for people who pay with Paypal? Want to make accounts for people who signed up on your HubSpot site? You're in the right place.

Subscribing users via Members API:

All the API examples are using the Ghost Admin API JavaScript SDK. Not shown each time is:

const api = new GhostAdminAPI({
    url: GHOST_API_URL,
    key: GHOST_API_KEY,
    version: "v5.93",
  
  });
const email = "myemailaddresshere";
let data = {
    name: "John Doe",
    email: email
  }

As you'll see below, none of these options for members.add requires the user to confirm their email address. Good or bad? Depends on your case.

Just the basics (including newsletters set to automatic subscribe):

  let data = {
    name: "John Doe",
    email: email
  }
  let options = {}
  
  let result = await api.members.add(data, options);
  console.log(result); // you'll get the newly created member object back.

This creates the user in Ghost. It'll send you an email (if you have that notification turned on in your profile), but it doesn't email the new member.

If you have any newsletters set to automatically subscribe, your user will be subscribed to them. If you're using this setup, make sure those emails are already opted in, ok?

Send the new user an email, too:

  let options = {
    send_email: true,
    email_type: 'subscribe'
  }

  let result = await api.members.add(data, options);
  console.log(result); // you'll get the newly created member object back.

This sends an email to the new user (with the "here's your link to confirm your subscription") but it actually creates them as a confirmed and subscribed member, whether or not they actually click the link.

Sign up for some different newsletters:

  let data = {
    name: "John Doe",
    email: email,
    newsletters: [{id: '66a4f73be87f2c0001dfb58a'}]
  }

Yes, you have to pass the newsletter id. You can get that by going to settings > newsletters and clicking the edit link. The newsletter id appears at the end of the URL.

Don't add default newsletters:

  let data = {
    name: "John Doe",
    email: "email",
    newsletters: []
  }
  let options = {
    send_email: true,
    email_type: 'subscribe'
  }

  let result = await api.members.add(data, options);

This creates the user as confirmed, but doesn't sign them up for any newsletters. I was hoping it'd trigger the popup to choose newsletters when they clicked the link, but it doesn't. If you're using this route, you'll want to make newsletter selection pretty obvious, for example, by linking it from your welcome page (if you have one). The direct link is: /#/portal/account/newsletters

Change the text of the email:

  let options = {
    send_email: true,
    email_type: 'signup'
  }

Changing the email_type to "signup" causes your new member to receive a new email telling them to click to complete their signup (instead of subscription), but otherwise changes nothing.

Sign up but send no email message:

  let options = {
    send_email: false,
  }

This creates the account (with any newsletters specified or newsletter defaults if nothing is specified), but doesn't send the new user an email.

The Admin API SDK doesn't send magic links, so we need to do more of the heavy lifting ourselves. Note that the endpoint here is the frontend endpoint. If your backend is on a different domain, take note!

async function sendMagicLink() {

  let result = await fetch(GHOST_FRONTEND_URL + '/members/api/send-magic-link/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'Jane Doe',
        email: email,
        emailType: 'subscribe',
        newsletters: [...]
        })
    })  
    console.log(result.status); // 201 and no body is the expected result
}

Passing in newsletters is optional. It'll default to whatever newsletters you have set to automatically subscribe if you omit it. See the API section for format.

This triggers the sending of a magic link to the user (with your choice of 'subscribe' or 'signup' language), and does NOT auto-confirm their account. They won't show up in your members list until they click the link.

If you need to confirm your users' emails and intent to receive email from you, this is the way to go about it.

Using Zapier?

Zapier's "Create Ghost User" action uses the Members API to create new members, and gives you the same options to send an email or not, and which language you want in that email (if you want one). If you need to run your users through a confirmation flow, it isn't what you need.

👉
None of the methods above will create a member with a Stripe subscription. Already have members in Stripe and need to get them into Ghost? Read on...

Importing members via CSV with automation.

You can import a CSV file of members (exported from Excel or similar) into Ghost. The easiest way to construct this is to download the sample file, add your users, then upload it. This is thoroughly documented over at Ghost.org, and if that meets your needs, you should have read the first paragraph of this post and then stopped, seriously. :)

But let's go back to trying to automate the Stripe problem. Suppose you need to get users into Ghost with their Stripe accounts intact, and you need to do it with a cloud function, not by logging into the dashboard. This is one of those spots where an API endpoint (with API key authentication) would be really nice. There isn't one, not really. But there is that members import functionality. Can we use it?

Welllll yess.... Here's how:

let filecontents = `id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,labels
    ,${useremail},${fullname},a note,,FALSE,${customerID},some sort of label`
    const mybody = new FormData();

    mybody.append("mapping[email]", "email");
    mybody.append("mapping[complimentary_plan]", "complimentary_plan");
    mybody.append("mapping[stripe_customer_id]", "stripe_customer_id");
    mybody.append("mapping[created_at]", "created_at");
    mybody.append("mapping[labels]", "labels");
    mybody.append('membersfile', Buffer.from(filecontents), { filename: 'data.csv' });

  let result = await fetch( GHOST_URL + '/ghost/api/admin/members/upload/', {
      method: 'POST',
      headers: {
        cookie: ADMINcookie,
        ...mybody.getHeaders()
        
      },
      body: mybody
    }) 
    console.log('posting CSV result is', result.message, result.statusText, result.error)

The big wrinkle here is that the API key doesn't work. You'll need to pass in the Ghost admin cookie instead.

One other note: The import runs asynchronously, so doesn't generate useful feedback on whether it succeeds or fails to whatever automation is running it. Nope, instead, it sends the user you ran it as an email.

And now, it's time to go clean up my inbox. Sooo much "you have a new member" spam!


Hey, before you go... If I just saved you a whole bunch of time and frustration, would you please keep this tea-drinking ghost and the freelancer behind her supplied with our hot beverage of choice? Much appreciated!