Vercel Cron Tutorial

Post to Twitter from a Vercel Cron Job
on a schedule

A crons entry, a secured route handler, and one fetch to OpenTweet. No OAuth 1.0a, no request signing, no X developer account.

7-day free trial • No credit card required

Vercel Cron calls a route, the route posts your tweet

Vercel Cron runs a schedule against a path in your own app. It hits that path, your route handler runs, and inside it you send one POST to OpenTweet's REST API athttps://opentweet.io/api/v1/posts. That is the whole loop.

Going straight to the X API from that handler means an approved developer account, OAuth 1.0a consumer keys, access tokens, and manual request signing on every call. That is a lot of ceremony to run inside a serverless function that only lives for a few seconds.

OpenTweet replaces all of that with a single ot_ key and JSON. Your cron handler stays tiny, and you secure it against outside calls with aCRON_SECRET.

vercel.json
{
  "crons": [
    {
      "path": "/api/cron/tweet",
      "schedule": "0 9 * * *"
    }
  ]
}
app/api/cron/tweet/route.ts
// Vercel calls this at 09:00 UTC daily.
// It just POSTs one tweet to OpenTweet.
await fetch("https://opentweet.io/api/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "Good morning from Vercel Cron!",
    publish_now: true,
  }),
});

Step-by-Step: Vercel Cron to Twitter

1

Get an ot_ API key

Sign up for OpenTweet, connect your X account, and open the developer page at opentweet.io/developer. Generate an API key. It starts with ot_ and is the only credential you need. You will add it to Vercel as an environment variable, never in your source.

bash
# Sanity check the key with GET /me before wiring the cron
curl https://opentweet.io/api/v1/me \
  -H "Authorization: Bearer ot_your_key"
2

Add a crons entry to vercel.json

Create a vercel.json at the root of your project with a crons array. Each entry has a path that Vercel calls and a cron schedule expression. The path below matches the route handler you build in the next step.

vercel.json
{
  "crons": [
    {
      "path": "/api/cron/tweet",
      "schedule": "0 9 * * *"
    }
  ]
}

Schedule is a cron expression

0 9 * * * runs once a day at 09:00 UTC. Vercel Cron schedules run in UTC, so pick your time accordingly.
3

Create a secured route handler

Add app/api/cron/tweet/route.ts. First check the incoming Authorization header against your CRON_SECRET so only Vercel Cron can trigger it, then POST to /api/v1/posts with your ot_ key.

app/api/cron/tweet/route.ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  // Vercel Cron sends "Bearer <CRON_SECRET>". Reject anything else.
  const auth = request.headers.get("authorization");
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const res = await fetch("https://opentweet.io/api/v1/posts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text: "Good morning from Vercel Cron!",
      publish_now: true,
    }),
  });

  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    // Do not retry permanent 4xx errors. Only 429 and 5xx are worth a retry.
    return NextResponse.json(
      { error: err.error || res.statusText },
      { status: res.status }
    );
  }

  return NextResponse.json(await res.json());
}

Always guard the route

The path is a public URL. Without the CRON_SECRET check, anyone who guesses it can fire your tweet. Compare the Authorization header before you do anything else.
4

Deploy to Vercel

In the Vercel dashboard, add two environment variables: OPENTWEET_API_KEY with your ot_ key and CRON_SECRET with a long random string. Then push. Vercel reads vercel.json on deploy and registers the cron.

bash
# Set the secrets, then deploy
vercel env add OPENTWEET_API_KEY
vercel env add CRON_SECRET
vercel --prod
5

Verify the cron runs

Open the Cron Jobs tab in your Vercel project to see the schedule and its last run. To confirm the OpenTweet side, list what is queued or check the key directly. A 200 from GET /me means the key is live.

bash
# Confirm the key works
curl https://opentweet.io/api/v1/me \
  -H "Authorization: Bearer ot_your_key"

# See everything the cron has queued
curl "https://opentweet.io/api/v1/posts?status=scheduled" \
  -H "Authorization: Bearer ot_your_key"

Queue for later instead of posting now

Swap publish_now for a scheduled_date in ISO 8601 format. The two are mutually exclusive. OpenTweet publishes at that time, so a nightly cron can fill tomorrow's queue.

app/api/cron/tweet/route.ts
await fetch("https://opentweet.io/api/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENTWEET_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "Scheduled by tonight's Vercel Cron run.",
    scheduled_date: "2026-07-10T09:00:00Z",
  }),
});

publish_now and scheduled_date are exclusive

Send one or the other, never both in the same request. Omit both and the post is saved as a draft you can edit later.

Pro Tips

Keep both secrets in Vercel env

Store OPENTWEET_API_KEY and CRON_SECRET as project environment variables, never in vercel.json or your source. Rotate the CRON_SECRET if you ever suspect the path leaked.

Backoff only on 429 and 5xx

A 400, 401, or 403 is permanent. Fix the request or the key, do not retry. Only retry 429 rate limits and 5xx errors, with exponential backoff between attempts.

Batch a week ahead

A weekly cron can POST to /posts/batch-schedule with up to 50 schedules at once, filling the whole week in a single run instead of one fetch per day.

Pipe an AI model into the handler

Generate copy with Claude inside the route, then POST the text with publish_now true. Your Vercel Cron becomes a hands-off content pipeline.

Common Mistakes to Avoid

Leaving the route unguarded

The cron path is a public URL. Skip the CRON_SECRET check and anyone who finds it can fire your tweet. Compare the Authorization header first and return 401 on a mismatch.

Retrying a permanent 4xx

A 400, 401, or 403 will fail every time until you fix the request. Retrying wastes runs and can hit your daily limit. Only retry 429 and 5xx with backoff.

Using api.opentweet.io

That host does not resolve. The REST base is https://opentweet.io/api/v1. Point every request there and keep your ot_ key in the Authorization header.

Frequently Asked Questions

How do I post to Twitter from a Vercel Cron Job?

Add a crons entry to vercel.json that points at a route handler like app/api/cron/tweet/route.ts. Inside the handler, verify the request came from Vercel Cron using a CRON_SECRET, then send a POST to https://opentweet.io/api/v1/posts with your ot_ key as a Bearer token and a JSON body like {"text":"Hello","publish_now":true}. No OAuth 1.0a and no X developer account are needed.

How does Vercel Cron authenticate the request?

Vercel Cron sends the request with an Authorization header set to Bearer plus the CRON_SECRET environment variable you configure. In your route handler, compare that header against process.env.CRON_SECRET and return 401 if it does not match, so no one but Vercel Cron can trigger the tweet.

Do I need the Twitter API or OAuth for a Vercel cron tweet?

No. The X API requires a developer account and OAuth 1.0a request signing on every call. OpenTweet uses a single ot_ Bearer key and plain JSON, so your Vercel route handler just does one fetch. There is no OAuth flow and no developer portal application to build.

Can Vercel Cron schedule tweets for the future instead of posting now?

Yes. In the POST body, send a scheduled_date in ISO 8601 format instead of publish_now. The two fields are mutually exclusive. OpenTweet publishes at that time, so your cron can queue posts ahead. To queue many at once, POST to /posts/batch-schedule with up to 50 schedules.

What if my cron hits a rate limit?

Over the per-minute or daily post limit, the API returns HTTP 429 Too Many Requests with a JSON error body. Do not retry permanent 4xx errors like 400, 401, or 403. Only retry 429 and 5xx responses, and use exponential backoff between attempts.

Ship your first cron tweet today

Get your ot_ key in 2 minutes. Wire it into a Vercel Cron Job in a few lines.