5 min read

Post Most Popular NY Times articles to Discord with QStash

Maximilian Kaske

Fullstack Developer (Guest Author)


In this post, we will build a simple application that will fetch the most popular viewed articles from the New York Times once a day via a cron job and post them to our internal discord #notifications channel to stay up to date with the outside world - creating discussions around non-technical hot topics.

CleanShot 2022-11-14 at 21.43.23.png

To build this, we'll use the new QStash Callback feature.

Quick recap': What is a callback function?

A callback function is a function that is accessible by another function and is invoked after the first function if that first function completes. It is “called at the back” of the passed function.


If you don't already have an existing Next.js application, you can create one with npx create-next-app@latest. We will use Next.js to write our API endpoint.

To build our example, we will need the QStash-SDK. Install it with the following:

npm install @upstash/qstash

QStash by Upstash is a powerful messaging and scheduling solution that we will use to create a cron job and include a callback URL that will be called once the published request has finished, including the response of the request.

Create the Callback API endpoint

The only part to code is the /api/callback URL to forward the message received to the Discord webhook.

Let's break the file down into the parts and start with the imports.

We will need verifySignature to verify that the request is coming from QStash and type the handler with the Next.js request and response types.

// pages/api/callback.ts_(1/4)
import type { NextApiRequest, NextApiResponse } from "next";
import { verifySignature } from "@upstash/qstash/nextjs";

The sendMessage function will forward the processed API response to our Discord channel.

Please bare with me that we'll use any as there is not something like a @types/nytimes-api to type the incoming request quickly. Read more about the supported properties at Moreover, I won't go too much into the Discord API possibilities. Feel free to explore the gist and extend the example with your use case.

// pages/api/callback.ts_(2/4)
async function sendMessage(json: any) {
  return fetch(process.env.DISCORD_WEBHOOK!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    body: JSON.stringify({
      username: "Best of NYTimes Bot",
      avatar_url: "", // random image generator
      // we will only post the five most popular articles of the day
      embeds: json.results.slice(0, 5).map((article: any) => {
        return {
          title: article.title,
          description: article.abstract,
          url: article.url,

Now that we have the helper function, let's continue with our request handler.

The request we are getting from QStash is base64 encoded. Once decoded, we can process the result and send the message to our Discord server.

// pages/api/callback.ts_(3/4)
async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    // requests from Upstash-Callback are base64-encoded
    const decoded = Buffer.from(req.body.body, "base64");
    const json = JSON.parse(decoded.toString());
    const result = await sendMessage(json);
    if (!result.ok) {
      return res.status(500).end();
    return res.status(201).end();
  } catch (e) {
    return res.status(500).end(e);

Finally, we export our handler, wrapped by the verifySignature function, that needs to access the raw-body of the request via the exported config (see Nextjs docs).

// pages/api/callback.ts_(4/4)
export const config = {
  api: {
    bodyParser: false,

export default verifySignature(handler);

Get the secrets / API keys

Go to your QStash dashboard and copy and paste the QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY into your .env.local environment variables.

For Discord, you can select the “Edit Channel” button and go to “Integrations > Webhooks” directly inside the App to “Copy [the] Webhook URL”.

CleanShot 2022-11-07 at 14.17.52@2x.png

CleanShot 2022-11-14 at 20.22.20.png

Your .env.local file should include the following:

## .env.local

Start the development server for the changes to take effect.

$ npm run dev

To access the New York Times API, you'll need to create an account at and create an App to access the API key. Be aware that your API is restricted by default. Enable the “Most Popular API”.

CleanShot 2022-11-14 at 20.11.00.png

You can check if it works by calling<your-key> in the browser or any API platform like postman.

Before configuring QStash, we have to publicly access our /api/callback. For that, we have two options:

  1. Either run it locally with an ngrok tunnel (because localhost is not publicly accessible)
$ ngrok http 3000 ## port number
  1. Or deploying it to a hosting provider (e.g. - don't forget to include the environment variables).

For the sake of this example, I've chosen the ngrok option.

Create the schedule

We can use the QStash Request Builder to start with and add the -H "Upstash-Callback:" \ value separately as it isn't included for now.

CleanShot 2022-11-14 at 22.00.18.png

Running the following command will then start our cron job.

curl -XPOST "" \
  -H "Upstash-Callback:" \ 
	-H "Upstash-Cron: 0 8 * * *" \ 
  -H "Authorization: Bearer XXX"

Let's decompose the request:

  1. QStash will call the NY Times API<your-key> and
  2. return the result to the defined callback at "Upstash-Callback:"
  3. every morning at 8 am via "Upstash-Cron: 0 8 * * *" (learn more at
  4. providing authorization with "Authorization: Bearer XXX" to allow our call to be also verified via verifySignature.


Let's wrap it up. Every morning at 8 am, QStash will request the most popular NY Times articles of the day and will forward the results to our callback URL (/api/callback) where we will post them to our Discord channel.

CleanShot 2022-11-21 at 10.15.19@2x.png

Check out the full example here.

© 2022 Upstash, Inc. Based in California.

* Redis is a trademark of Redis Labs Ltd. Any rights therein are reserved to Redis Ltd. Any use by Upstash is for referential purposes only and does not indicate any sponsorship, endorsement or affiliation between Redis and Upstash.

** Cloudflare, the Cloudflare logo, and Cloudflare Workers are trademarks and/or registered trademarks of Cloudflare, Inc. in the United States and other jurisdictions.