DEV Community

Rohan sirohi
Rohan sirohi

Posted on

How to Schedule Social Media Posts to Instagram, TikTok, and YouTube with Node.js

Scheduling a social media post sounds simple. You pick a time, you publish at that time. But if you've tried to build this yourself, you know there are at least three ways it can quietly fail:

  1. Your cron job runs but the token expired overnight
  2. The video is still processing on Instagram's side when you try to publish
  3. Your server restarts and the in-memory job queue is gone

This tutorial covers a robust approach to scheduling posts to Instagram, TikTok, and YouTube using Node.js — including how to handle all three failure modes above.

Prerequisites

  • Node.js 18+
  • An InReelForge account (free tier works) — indreelforge.com
  • Your social accounts connected in the InReelForge dashboard

Setup

mkdir social-scheduler && cd social-scheduler
npm init -y
npm install indreelforge dotenv
Enter fullscreen mode Exit fullscreen mode

Create .env:

INDREELFORGE_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

Basic Scheduling

The simplest case — schedule a post with a future timestamp:

import InReelForge from 'indreelforge';
import 'dotenv/config';

const client = new InReelForge({ apiKey: process.env.INDREELFORGE_API_KEY });

async function schedulePost(videoPath, publishAt) {
  const result = await client.publish({
    video: videoPath,
    platforms: ['instagram', 'tiktok', 'youtube'],
    caption: 'New video dropping now 🎬',
    hashtags: ['content', 'video'],
    schedule: publishAt.toISOString(),
  });

  console.log('Scheduled:', result.jobId);
  console.log('Status per platform:', result.platforms);
  return result;
}

// Schedule for tomorrow at 9am UTC
const tomorrow9am = new Date();
tomorrow9am.setUTCDate(tomorrow9am.getUTCDate() + 1);
tomorrow9am.setUTCHours(9, 0, 0, 0);

await schedulePost('./my-video.mp4', tomorrow9am);
Enter fullscreen mode Exit fullscreen mode

The schedule is persisted server-side in a database-backed queue — if InReelForge restarts, your job survives. You don't need to run your own scheduler.

Per-Platform Scheduling

Different platforms have different peak times. You can send each platform its own schedule:

const result = await client.publish({
  video: './video.mp4',
  caption: 'Default caption',
  platforms: ['instagram', 'tiktok', 'youtube'],
  overrides: {
    instagram: {
      schedule: '2026-06-01T11:00:00Z', // 11am UTC
      caption: 'Instagram-specific caption with #reels #viral',
    },
    tiktok: {
      schedule: '2026-06-01T18:00:00Z', // 6pm UTC — peak TikTok time
      caption: 'TikTok hook caption here',
    },
    youtube: {
      schedule: '2026-06-01T14:00:00Z', // 2pm UTC
      caption: 'Full YouTube description with timestamps\n\n0:00 Intro\n0:30 Main content',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Building a Simple Content Queue

If you're managing a backlog of content, here's a pattern for a queue processor:

import { readdir, readFile } from 'fs/promises';
import path from 'path';
import InReelForge from 'indreelforge';

const client = new InReelForge({ apiKey: process.env.INDREELFORGE_API_KEY });

// Each file in /queue/ is a JSON job descriptor
async function processQueue(queueDir) {
  const files = await readdir(queueDir);
  const pending = files.filter(f => f.endsWith('.json'));

  for (const file of pending) {
    const job = JSON.parse(await readFile(path.join(queueDir, file), 'utf8'));

    try {
      const result = await client.publish({
        video: job.videoPath,
        platforms: job.platforms,
        caption: job.caption,
        hashtags: job.hashtags,
        schedule: job.publishAt,
      });

      console.log(`✓ Scheduled ${file} → jobId: ${result.jobId}`);

      // Move to /done/
      await rename(
        path.join(queueDir, file),
        path.join(queueDir, '../done', file)
      );
    } catch (err) {
      console.error(`✗ Failed ${file}:`, err.message);
    }
  }
}

await processQueue('./queue');
Enter fullscreen mode Exit fullscreen mode

A job JSON file (queue/post-001.json) looks like:

{
  "videoPath": "./videos/demo.mp4",
  "platforms": ["instagram", "tiktok", "youtube"],
  "caption": "New video — check it out!",
  "hashtags": ["saas", "demo"],
  "publishAt": "2026-06-01T09:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Checking Job Status

After scheduling, poll for results or set up a webhook:

// Poll approach
async function waitForResult(jobId, intervalMs = 5000) {
  while (true) {
    const status = await client.getJob(jobId);

    const allDone = Object.values(status.platforms).every(
      s => s === 'published' || s === 'failed'
    );

    if (allDone) return status;

    console.log('Still processing...', status.platforms);
    await new Promise(r => setTimeout(r, intervalMs));
  }
}

const final = await waitForResult(result.jobId);
console.log('Final status:', final.platforms);
Enter fullscreen mode Exit fullscreen mode

Webhook Approach (Better for Production)

Instead of polling, receive a callback when each platform finishes:

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.raw({ type: 'application/json' }));

app.post('/webhooks/indreelforge', (req, res) => {
  const sig = req.headers['x-irf-signature'];
  const digest = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(digest))) {
    return res.status(403).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log(`${event.platform}: ${event.status}`);

  // event.status: 'published' | 'failed'
  // event.platform: 'instagram' | 'tiktok' | 'youtube' etc.
  // event.url: public URL of the published post (when published)

  res.sendStatus(200);
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Set your webhook URL in the InReelForge dashboard under Settings → Webhooks.

Common Mistakes

Don't schedule in the past. The API will reject timestamps more than 5 minutes behind the current time.

Don't assume immediate publishing. Even "publish now" goes through a queue — Instagram and TikTok take 30–120 seconds to process video.

Do validate video before scheduling. Use the /validate endpoint to check format compatibility before uploading:

const validation = await client.validate({
  video: './video.mp4',
  platforms: ['instagram', 'tiktok'],
});

if (!validation.compatible) {
  console.log('Issues:', validation.issues);
  // e.g. [{ platform: 'instagram', issue: 'aspect_ratio', detail: 'Expected 9:16, got 16:9' }]
}
Enter fullscreen mode Exit fullscreen mode

Full Example Script

import InReelForge from 'indreelforge';
import 'dotenv/config';

const client = new InReelForge({ apiKey: process.env.INDREELFORGE_API_KEY });

const video = './product-launch.mp4';
const platforms = ['instagram', 'tiktok', 'youtube'];
const publishAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24h from now

// 1. Validate
const check = await client.validate({ video, platforms });
if (!check.compatible) {
  console.error('Video not compatible:', check.issues);
  process.exit(1);
}

// 2. Schedule
const job = await client.publish({
  video,
  platforms,
  caption: 'We just launched something big. Check it out.',
  hashtags: ['launch', 'saas', 'buildinpublic'],
  schedule: publishAt,
  overrides: {
    youtube: { caption: 'Full product demo with timestamps...' },
  },
});

console.log(`Scheduled ${platforms.length} posts for ${publishAt}`);
console.log(`Job ID: ${job.jobId}`);
Enter fullscreen mode Exit fullscreen mode

Full API reference: docs.indreelforge.com

Free tier — 10 posts/month: indreelforge.com

Top comments (0)