ScreenshotAPI

Vercel / Next.js Integration

Generate screenshots in your Next.js app deployed on Vercel using API routes and server components.

Overview

This guide shows how to integrate ScreenshotAPI into a Next.js application deployed on Vercel. You'll create an API route that proxies screenshot requests, keeping your API key secure on the server.

Setup

Add your API key to environment variables

In your Vercel project settings or .env.local:

SCREENSHOTAPI_KEY=sk_live_your_key_here

Create the API route

Create app/api/screenshot/route.ts:

import { NextResponse, type NextRequest } from 'next/server'

const SCREENSHOTAPI_KEY = process.env.SCREENSHOTAPI_KEY!
const BASE_URL = 'https://screenshotapi.to'

export async function GET(request: NextRequest) {
  const url = request.nextUrl.searchParams.get('url')

  if (!url) {
    return NextResponse.json({ error: 'url is required' }, { status: 400 })
  }

  const params = new URLSearchParams({ url })

  // Forward optional parameters
  const optionalParams = [
    'width', 'height', 'fullPage', 'type', 'quality',
    'colorScheme', 'waitUntil', 'waitForSelector', 'delay'
  ]

  for (const param of optionalParams) {
    const value = request.nextUrl.searchParams.get(param)
    if (value) params.set(param, value)
  }

  try {
    const response = await fetch(
      `${BASE_URL}/api/v1/screenshot?${params}`,
      { headers: { 'x-api-key': SCREENSHOTAPI_KEY } }
    )

    if (!response.ok) {
      const error = await response.json()
      return NextResponse.json(error, { status: response.status })
    }

    const buffer = await response.arrayBuffer()

    return new NextResponse(buffer, {
      headers: {
        'Content-Type': response.headers.get('content-type') ?? 'image/png',
        'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
        'x-credits-remaining': response.headers.get('x-credits-remaining') ?? '',
        'x-duration-ms': response.headers.get('x-duration-ms') ?? ''
      }
    })
  } catch {
    return NextResponse.json({ error: 'Screenshot failed' }, { status: 500 })
  }
}

Use from your frontend

function ScreenshotImage({ url }: { url: string }) {
  const src = `/api/screenshot?url=${encodeURIComponent(url)}&type=webp&quality=80`

  return (
    <img
      src={src}
      alt={`Screenshot of ${url}`}
      loading="lazy"
      className="rounded-lg border shadow-sm"
    />
  )
}

Server Component Usage

Fetch screenshots directly in React Server Components:

async function ServerScreenshot({ url }: { url: string }) {
  const params = new URLSearchParams({
    url,
    type: 'webp',
    quality: '80'
  })

  const response = await fetch(
    `https://screenshotapi.to/api/v1/screenshot?${params}`,
    {
      headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! },
      next: { revalidate: 3600 }
    }
  )

  if (!response.ok) {
    return <div className="text-red-500">Failed to load screenshot</div>
  }

  const buffer = await response.arrayBuffer()
  const base64 = Buffer.from(buffer).toString('base64')
  const contentType = response.headers.get('content-type') ?? 'image/webp'
  const dataUri = `data:${contentType};base64,${base64}`

  return (
    <img
      src={dataUri}
      alt={`Screenshot of ${url}`}
      className="rounded-lg border shadow-sm"
    />
  )
}

Using next: { revalidate: 3600 } caches the screenshot for 1 hour with Next.js ISR, reducing API calls and credit usage.

Caching Strategies

Edge Caching with Vercel

Set appropriate cache headers to leverage Vercel's edge network:

return new NextResponse(buffer, {
  headers: {
    'Content-Type': contentType,
    // Cache for 1 hour, serve stale for 24 hours while revalidating
    'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
    // Vary by query params so different URLs get different cache entries
    'Vary': 'Accept'
  }
})

In-Memory Cache with Map

For high-frequency screenshots of the same URLs:

const cache = new Map<string, { buffer: ArrayBuffer; contentType: string; timestamp: number }>()
const CACHE_TTL = 60 * 60 * 1000 // 1 hour

async function cachedScreenshot(url: string): Promise<{ buffer: ArrayBuffer; contentType: string }> {
  const cached = cache.get(url)
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return { buffer: cached.buffer, contentType: cached.contentType }
  }

  const response = await fetch(
    `https://screenshotapi.to/api/v1/screenshot?url=${encodeURIComponent(url)}`,
    { headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! } }
  )

  const buffer = await response.arrayBuffer()
  const contentType = response.headers.get('content-type') ?? 'image/png'

  cache.set(url, { buffer, contentType, timestamp: Date.now() })
  return { buffer, contentType }
}

Vercel Function Configuration

For long pages or slow-loading sites, increase the function timeout:

// app/api/screenshot/route.ts
export const maxDuration = 30 // seconds (available on Pro plan)

Vercel Hobby plans have a 10-second function timeout. If your screenshots take longer (full-page captures of large sites), consider upgrading to Pro for the 60-second limit.

A complete link preview component that generates and caches thumbnails:

import { Suspense } from 'react'

function LinkPreviewSkeleton() {
  return (
    <div className="animate-pulse rounded-lg border bg-muted h-[200px] w-full" />
  )
}

async function LinkPreviewImage({ url }: { url: string }) {
  const params = new URLSearchParams({
    url,
    width: '800',
    height: '600',
    type: 'webp',
    quality: '75'
  })

  const response = await fetch(
    `https://screenshotapi.to/api/v1/screenshot?${params}`,
    {
      headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! },
      next: { revalidate: 86400 }
    }
  )

  if (!response.ok) return null

  const buffer = Buffer.from(await response.arrayBuffer())
  const base64 = buffer.toString('base64')
  const src = `data:image/webp;base64,${base64}`

  return (
    <img src={src} alt="" className="rounded-lg object-cover w-full h-[200px]" />
  )
}

export function LinkPreview({ url, title }: { url: string; title: string }) {
  return (
    <a href={url} className="block group cursor-pointer">
      <div className="overflow-hidden rounded-lg border transition-shadow group-hover:shadow-md">
        <Suspense fallback={<LinkPreviewSkeleton />}>
          <LinkPreviewImage url={url} />
        </Suspense>
        <div className="p-3">
          <p className="font-medium text-sm truncate">{title}</p>
          <p className="text-xs text-muted-foreground truncate">{url}</p>
        </div>
      </div>
    </a>
  )
}

On this page