Node.js API April 5, 2026

Automate Social Media Screenshots with Node.js (2026)

Capture Twitter/X posts, LinkedIn profiles, Instagram content, and more — programmatically. Compare Playwright, embed APIs, and dedicated screenshot services.

Why Social Media Screenshots Are Hard

Social media platforms are hostile to automated capture. They load content dynamically via JavaScript, require authentication for most profiles, aggressively detect and block headless browsers, and serve different layouts based on viewport, geolocation, and login state. A naive page.screenshot() often captures a login wall or an empty shell.

PlatformMain ChallengeAuth Required?
Twitter/XLogin wall after few views, rate limitingUsually yes
LinkedInAggressive bot detection, AuthwallYes
InstagramLogin required for most contentYes
FacebookHeavy JS rendering, privacy wallsUsually yes
RedditOld/new Reddit layouts, interstitialsNo (public posts)
YouTubeConsent dialogs, dynamic loadingNo (public videos)

Capturing Twitter/X Posts

Twitter has an official embed API that renders tweet cards without authentication. This is the most reliable approach — no login, no bot detection.

const { chromium } = require('playwright');

async function captureTweet(tweetUrl) {
  const tweetId = tweetUrl.split('/status/')[1]?.split('?')[0];
  if (!tweetId) throw new Error('Invalid tweet URL');

  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 550, height: 800 } });

  // Use Twitter's embed endpoint — no auth needed
  const embedHtml = `
    <!DOCTYPE html>
    <html><head><style>body{margin:0;background:#0a0a0f;display:flex;justify-content:center;padding:20px;}</style></head>
    <body>
      <blockquote class="twitter-tweet" data-theme="dark">
        <a href="${tweetUrl}"></a>
      </blockquote>
      <script src="https://platform.twitter.com/widgets.js"></script>
    </body></html>`;

  await page.setContent(embedHtml);
  // Wait for Twitter widget to render
  await page.waitForSelector('iframe.twitter-tweet-rendered', { timeout: 15000 });
  await page.waitForTimeout(2000); // Let images load

  const frame = page.frames().find(f => f.url().includes('platform.twitter.com'));
  const tweetElement = frame
    ? await frame.$('article')
    : await page.$('.twitter-tweet-rendered');

  const screenshot = tweetElement
    ? await tweetElement.screenshot()
    : await page.screenshot({ fullPage: true });

  await browser.close();
  return screenshot;
}

For bulk captures, maintain a browser instance and reuse pages. Twitter rate-limits embed renders too — add 2-3 second delays between captures.

LinkedIn Profile Screenshots

LinkedIn is one of the hardest platforms to capture. It blocks headless browsers aggressively and requires authentication for profile views. The safest approach uses stored session cookies.

async function captureLinkedIn(profileUrl, cookiesPath) {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    storageState: cookiesPath, // Saved from a real browser session
    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
    viewport: { width: 1280, height: 900 }
  });
  const page = await context.newPage();

  await page.goto(profileUrl, { waitUntil: 'networkidle' });

  // Close any modals (LinkedIn loves modals)
  const modal = await page.$('[data-test-modal-close-btn]');
  if (modal) await modal.click();
  await page.waitForTimeout(1000);

  // Capture just the profile card
  const profileCard = await page.$('.pv-top-card');
  const screenshot = profileCard
    ? await profileCard.screenshot()
    : await page.screenshot({ clip: { x: 0, y: 0, width: 1280, height: 600 } });

  await browser.close();
  return screenshot;
}

Export cookies from your browser using a cookie editor extension, save as Playwright's storageState JSON format. Sessions last ~30 days before re-auth is needed. Don't hit more than ~50 profiles/hour or you'll trigger LinkedIn's bot detection.

Multi-Platform Capture Class

Build a single class that handles different platforms with platform-specific wait strategies and element selectors.

class SocialCapture {
  constructor() { this.browser = null; }

  async init() {
    this.browser = await chromium.launch();
  }

  async capture(url) {
    const platform = this.detectPlatform(url);
    const page = await this.browser.newPage({
      viewport: platform.viewport
    });

    try {
      await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });

      // Platform-specific waits
      if (platform.waitSelector) {
        await page.waitForSelector(platform.waitSelector, { timeout: 10000 })
          .catch(() => {});
      }

      // Dismiss common overlays
      for (const sel of platform.dismissSelectors || []) {
        const el = await page.$(sel);
        if (el) await el.click().catch(() => {});
      }
      await page.waitForTimeout(1500);
      const element = platform.captureSelector
        ? await page.$(platform.captureSelector)
        : null;

      return element
        ? await element.screenshot()
        : await page.screenshot({ fullPage: false });
    } finally {
      await page.close();
    }
  }

  detectPlatform(url) {
    const platforms = {
      'twitter.com': { viewport: { width: 600, height: 900 },
        waitSelector: 'article', captureSelector: 'article',
        dismissSelectors: ['[data-testid="sheetDialog"] [role="button"]'] },
      'x.com': { viewport: { width: 600, height: 900 },
        waitSelector: 'article', captureSelector: 'article',
        dismissSelectors: ['[data-testid="sheetDialog"] [role="button"]'] },
      'linkedin.com': { viewport: { width: 1280, height: 900 },
        waitSelector: '.feed-shared-update-v2',
        dismissSelectors: ['[data-test-modal-close-btn]'] },
      'reddit.com': { viewport: { width: 800, height: 1000 },
        waitSelector: 'shreddit-post',
        dismissSelectors: ['[id="accept-all"]', '.XPromoPopup button'] },
      'youtube.com': { viewport: { width: 1280, height: 720 },
        waitSelector: '#player', captureSelector: '#player',
        dismissSelectors: ['[aria-label="Accept all"]', 'tp-yt-paper-dialog .yt-spec-button-shape-next'] }
    };
    const host = new URL(url).hostname.replace('www.', '');
    return platforms[host] || { viewport: { width: 1280, height: 900 } };
  }

  async close() { if (this.browser) await this.browser.close(); }
}

SnapAPI: Skip the Complexity

All the platform-specific logic above is fragile — selectors change, bot detection evolves, sessions expire. SnapAPI handles all of this with stealth mode, cookie blocking, and automatic wait strategies.

// Capture any social media post — one API call
const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': 'sk_live_your_key'
  },
  body: JSON.stringify({
    url: 'https://x.com/elonmusk/status/1234567890',
    fullPage: false,
    stealth: true,
    blockAds: true,
    blockCookieBanners: true,
    device: 'desktop',
    format: 'png'
  })
});

const imageBuffer = await response.arrayBuffer();
fs.writeFileSync('tweet.png', Buffer.from(imageBuffer));
# Python — batch capture social posts
import httpx, asyncio

async def capture_social(urls):
    async with httpx.AsyncClient(timeout=30) as client:
        tasks = [
            client.post('https://api.snapapi.pics/v1/screenshot',
                headers={'X-Api-Key': 'sk_live_your_key'},
                json={'url': url, 'stealth': True, 'blockAds': True, 'blockCookieBanners': True}
            )
            for url in urls
        ]
        responses = await asyncio.gather(*tasks)
    for i, resp in enumerate(responses):
        if resp.status_code == 200:
            with open(f'social_{i}.png', 'wb') as f:
                f.write(resp.content)

asyncio.run(capture_social([
    'https://x.com/OpenAI/status/123456',
    'https://www.reddit.com/r/programming/comments/abc123/',
    'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
]))

SnapAPI's stealth mode handles headless detection bypass, and the blockCookieBanners flag removes consent dialogs that would otherwise cover the content. 200 free requests/month, no browser infrastructure to maintain.