Skip to contentVisit my new website at chrisnicholas.dev
Article posted on

Integrate Twitter Newsletters with API Routes
How to add Revue newsletter subscribers with Vercel API routes

Last week newsletters started popping up on Twitter profiles, using their recently purchased service, Revue. But how do you create a newsletter and add subscribers from your website? Using Vercel API routes you can start collecting subscribers in minutes. Here's a quick guide.
Table of contents

Last week newsletters started popping up on Twitter profiles, using their recently purchased service, Revue. But how do you create a newsletter and add subscribers from your website? Using Vercel API routes you can start collecting subscribers in minutes. Here's a quick guide.

Shiba Space Weekly
Example of a custom subscription form

Twitter & Revue

Earlier this year Twitter purchased Revue, a newsletter service, and last week they started integrating newsletter links into Twitter profiles:

I've been giving it a try, and I quite like it. It may not have the advanced functionality of some of its competitors yet, but it does everything I need to quickly send out articles to my followers, along with having a very generous free plan—so I've made the switch from Mailchimp.

The free plan offers unlimited newsletters (or issues), unlimited subscribers, custom from addresses, and a basic newsletter editor. It also allowed me to quickly import my Mailchimp subscribers.

Sign up to Revue

First we need to sign up to Revue; I'd recommend registering with your Twitter account, if you're planning to attach it to your profile. After signing up, go to Account settings then Integrations (or click here), and find your API key, right at the bottom of the page (it'll be around this length):

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

If you don't have a project ready, and would like to follow along, I've created a Next.js template to use:

Edit on CodeSandbox
Integrate Twitter Newsletters into your Website, initial Next.js template

Setup

To connect to Revue, we'll be using fetch and FormData, two Web APIs (we'll get into why later). Neither of these are supported in Node.js, so we'll install two polyfills:

npm install node-fetch form-data

I'm also choosing to store my secret API key as an environment variable in my project:

.env.local
REVUE_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Create API route

Great, we can get started with the subscription API route. From the front-end we'll send a JSON object similar to this:

{ "email": "email@example.com" }

We can then create a Vercel API route at /api/subscribe.js, retrieve the email using req.body, and send an error if no email has been submitted:

export default async function (req, res) {
  const { email } = JSON.parse(req.body)

  if (!email) {
    res.status(400).json({ error: 'No email submitted' })
    return
  }
  
  // ...
}

Sending Revue API request

All POST requests sent to the Revue API require multipart/form-data encoding, which is why we're using FormData as the body of the request:

// Create body to be sent to Revue
const formData = new FormData()
formData.append('email', email)
formData.append('double_opt_in', 'false')

Revue also requires an Authorization header, which we can add to the fetch options. We'll then create data, the parsed JSON sent from Revue:

const url = 'https://www.getrevue.co/api/v2/subscribers'
const secret = process.env.REVUE_SECRET_KEY

// Call Revue API, get result & data
const result = await fetch(url, {
  method: 'POST',
  headers: {
    Authorization: `Token ${secret}`
  },
  // `formData` from above
  body: formData
})
const data = await result.json()

Returning the result

Finally, we'll check to see if the request worked, and then return an error, or the data:

// Revue API error, send error
if (!result.ok) {
  res.status(500).json({ error: data.error.email[0] })
  return
}

// Success, send data
res.status(200).json({ data })

Putting it together

If we put everything together, it looks like this:

import FormData from 'form-data'
import fetch from 'node-fetch'

const url = 'https://www.getrevue.co/api/v2/subscribers'
const secret = process.env.REVUE_SECRET_KEY

export default async function (req, res) {
  const { email } = JSON.parse(req.body)

  if (!email) {
    res.status(400).json({ error: 'No email submitted' })
    return
  }

  const formData = new FormData()
  formData.append('email', email)
  formData.append('double_opt_in', 'false')

  const result = await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Token ${secret}`
    },
    body: formData
  })
  const data = await result.json()

  if (!result.ok) {
    res.status(500).json({ error: data.error.email[0] })
    return
  }

  res.status(200).json({ data })
}

We now have a working API route!

Connect the front-end

Connecting the front-end can be done within an async function. Because we return either a data or an error property from the API route (but not both), error checking is neat and tidy:

async function addSubscriber (email) {
  // The location of your API route
  const url = '/api/subscribe'

  const { data, error } = await fetch(url, {
    method: 'POST',
    body: JSON.stringify({ email })
  }).then(res => res.json())

  if (error) {
    console.log('Error:', error)
    return
  }

  console.log('Success:', data)
}

Done! We've successfully connected to Revue, and it's time to get building subscribe forms. Here's an example newsletter box with a sample API return value:

Shiba Space Weekly
Example of API route input and output
Sent to API route:
{
  "email": ""
}
Received from API route:
{
  "data": {
    "id": 276564104,
    "list_id": 300978,
    "email": "",
    "first_name": null,
    "last_name": null,
    "last_changed": "2024-02-08T09:30:41.203Z"
  }
}

After trying out your new system, take a look at the subscribers page on Revue to see your new subscribers.

Get newsletter history

The Revue API allows for other functions too, for example we can grab a list of previously posted newsletters, and write them to the page. Here's a live example of my previous issues, with links:

This can be implemented using the issues part of the API:

Get all issues
import fetch from 'node-fetch'

const url = 'https://www.getrevue.co/api/v2/issues'
const secret = process.env.REVUE_SECRET_KEY

export default async function (req, res) {
  const result = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Token ${secret}`
    }
  })
  const data = await result.json()

  if (!result.ok) {
    res.status(500).json({ error: data.error })
    return
  }

  // Returns array of all issues
  res.status(200).json({ data })
}
Get all issues (client)
async function getAllIssues (email) {
  // The location of your API route
  const url = '/api/issues'

  const { data, error } = await fetch(url).then(res => res.json())

  if (error) {
    console.log('Error:', error)
    return
  }

  console.log('Success:', data)
}

Pretty easy stuff!

Snippets

Here's some more Revue API routes that allow you to implement other features:

Get latest issue
import fetch from 'node-fetch'

const url = 'https://www.getrevue.co/api/v2/issues/latest'
const secret = process.env.REVUE_SECRET_KEY

export default async function (req, res) {
  const result = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Token ${secret}`
    }
  })
  const { issue, error } = await result.json()

    if (!result.ok) {
      res.status(500).json({ error })
      return
    }

    // Returns array containing one issue
    res.status(200).json({ data: issue })
  }
Get profile URL
import fetch from 'node-fetch'

const url = 'https://www.getrevue.co/api/v2/accounts/me'
const secret = process.env.REVUE_SECRET_KEY

export default async function (req, res) {
  const result = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Token ${secret}`
    }
  })
  const { profile_url, error } = await result.json()

  if (!result.ok) {
    res.status(500).json({ error })
    return
  }

  // Returns string e.g. "https://www.getrevue.co/profile/ctnicholasdev"
  res.status(200).json({ data: profile_url })
}
Get all subscribers (best keep this on the back-end!)
import fetch from 'node-fetch'

const url = 'https://www.getrevue.co/api/v2/subscribers'
const secret = process.env.REVUE_SECRET_KEY

export default async function (req, res) {
  const result = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Token ${secret}`
    }
  })
  const data = await result.json()

  if (!result.ok) {
    console.log(data.error)
    res.status(500).json({ error: 'Error retrieving subscribers' })
    return
  }

  // `data` is an array of subscribers
  const backendResult = doBackendStuff(data)
  res.status(200).json({ data: backendResult })
}
Add an item to the latest unpublished newsletter
import fetch from 'node-fetch'
import FormData from 'form-data'

const latestUrl = 'https://www.getrevue.co/api/v2/issues/current'
const itemUrl = id => `https://www.getrevue.co/api/v2/issues/${id}/items`
const secret = process.env.REVUE_SECRET_KEY

export default async function (req, res) {
  const id = await getLatestId()

  if (id.error) {
    res.status(500).json({ error: id.error })
    return
  }

  // Get this into from JSON.parse(req.body) in your actual project (except id)
  const exampleData = {
    id: id.data,
    url: 'https://ctnicholas.dev',
    image: 'iVBORw0KGgoAAAANSUhE..', // Base 64 image
    caption: 'Visit my blog!'
  }

  const formData = createFormData(exampleData)
  const result = await fetch(itemUrl(id), {
    method: 'POST',
    headers: {
      Authorization: `Token ${secret}`
    },
    body: formData
  })
  const data = await result.json()

  if (!result.ok) {
    res.status(500).json({ error: data.error })
    return
  }

  // Returns object containing the issue data added
  res.status(200).json({ data })
}

// Create FormData for edit issue API
function createFormData ({ id, url, image, caption }) {
  const fields = {
    issue_id: id,
    type: image ? 'image' : '',
    caption,
    image,
    url
  }

  const formData = new FormData()
  Object.entries(fields).forEach(([key, val]) => {
    if (val) {
      formData.append(key, val)
    }
  })
  return formData
}

// Get the id of the current non-published issue
async function getLatestId () {
  const result = await fetch(latestUrl, {
    method: 'GET',
    headers: {
      Authorization: `Token ${secret}`
    }
  })
  const data = await result.json()

  if (data.error) {
    return { error: data.error }
  }

  // Returns a Number e.g. 737051
  return { data: data[0].id }
}

Summary

With Revue, setting up a newsletter is easier (and much cheaper) than ever. I hope you've enjoyed reading this article (and playing with the Shiba, I enjoyed building that!), and if you'd like to try a real Revue subscribe form... there's one below!