DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to screenshot authenticated pages (login-gated content)

How to Screenshot Authenticated Pages

Capturing a public URL is straightforward. Capturing a dashboard, invoice, or any page that requires login is where headless browser automation breaks down: you need to log in first, preserve the session cookie, pass it to the screenshot request, and handle session expiry.

PageBolt lets you pass cookies and headers directly — no login automation required.

Pass a session cookie

If your app uses cookie-based sessions (most server-rendered apps do):

const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
  method: "POST",
  headers: {
    "x-api-key": process.env.PAGEBOLT_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/dashboard",
    cookies: [
      {
        name: "session",
        value: "your-session-token-here",
        domain: "yourapp.com",
      },
    ],
    fullPage: true,
    blockBanners: true,
  }),
});

const image = Buffer.from(await res.arrayBuffer());
Enter fullscreen mode Exit fullscreen mode

Pass a Bearer token via headers

For SPAs and APIs that use Authorization headers:

const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
  method: "POST",
  headers: {
    "x-api-key": process.env.PAGEBOLT_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/reports/q4-2024",
    headers: {
      Authorization: `Bearer ${userToken}`,
    },
    fullPage: true,
  }),
});
Enter fullscreen mode Exit fullscreen mode

Multiple cookies (e.g., CSRF + session)

Some frameworks require both a session cookie and a CSRF token:

body: JSON.stringify({
  url: "https://yourapp.com/admin/users",
  cookies: [
    { name: "session_id", value: sessionId, domain: "yourapp.com" },
    { name: "csrf_token", value: csrfToken, domain: "yourapp.com" },
  ],
}),
Enter fullscreen mode Exit fullscreen mode

Generate a screenshot from inside your app (server-side)

The cleanest pattern for internal tools: generate a short-lived token on the server, embed it in the URL, screenshot the page, expire the token:

// Express route — generates signed URL, screenshots it, returns PDF
app.get("/invoices/:id/pdf", requireAuth, async (req, res) => {
  const { id } = req.params;

  // Create a single-use signed token
  const token = await db.signedTokens.create({
    invoiceId: id,
    userId: req.user.id,
    expiresAt: new Date(Date.now() + 60_000), // 1 minute
  });

  const signedUrl = `https://yourapp.com/invoices/${id}?token=${token.value}`;

  const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
    method: "POST",
    headers: {
      "x-api-key": process.env.PAGEBOLT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: signedUrl }),
  });

  const pdf = Buffer.from(await pdfRes.arrayBuffer());

  // Expire the token immediately
  await db.signedTokens.expire(token.id);

  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename="invoice-${id}.pdf"`);
  res.send(pdf);
});
Enter fullscreen mode Exit fullscreen mode

This avoids passing long-lived credentials to a third-party service — the token is valid for 60 seconds and invalidated after use.

Capture a user's dashboard on their behalf

For SaaS apps that need to email users a screenshot of their account state:

async function captureUserDashboard(userId) {
  // Get the user's current session token from your DB
  const user = await db.users.findById(userId);
  const sessionToken = await generateSessionToken(user);

  const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
    method: "POST",
    headers: {
      "x-api-key": process.env.PAGEBOLT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: `https://yourapp.com/users/${userId}/dashboard`,
      cookies: [
        {
          name: "session",
          value: sessionToken,
          domain: "yourapp.com",
        },
      ],
      fullPage: true,
      blockBanners: true,
      blockAds: true,
    }),
  });

  return Buffer.from(await res.arrayBuffer());
}

// Send weekly dashboard screenshot to all users
async function sendWeeklyDigest() {
  const users = await db.users.findActive();
  for (const user of users) {
    const image = await captureUserDashboard(user.id);
    await email.send({
      to: user.email,
      subject: "Your weekly summary",
      attachments: [{ filename: "dashboard.png", content: image }],
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Python example

import os
import requests

def screenshot_authenticated(url: str, session_cookie: str) -> bytes:
    resp = requests.post(
        "https://pagebolt.dev/api/v1/screenshot",
        headers={"x-api-key": os.environ["PAGEBOLT_API_KEY"]},
        json={
            "url": url,
            "cookies": [{"name": "session", "value": session_cookie, "domain": url.split("/")[2]}],
            "fullPage": True,
            "blockBanners": True,
        },
    )
    resp.raise_for_status()
    return resp.content

image = screenshot_authenticated(
    "https://yourapp.com/dashboard",
    session_cookie="your-session-token"
)
with open("dashboard.png", "wb") as f:
    f.write(image)
Enter fullscreen mode Exit fullscreen mode

What doesn't work (and why)

The alternative — automating login via a sequence of clicks — is fragile:

  • Login forms change layout → automation breaks
  • CAPTCHAs and bot detection block headless browsers
  • MFA flows are hard to automate
  • Session cookies from automated logins expire differently than API tokens

Passing a cookie or header directly sidesteps all of that. Generate a token server-side where you control the auth logic, pass it to the capture request, and expire it immediately after.


Try it free — 100 requests/month, no credit card. → Get started in 2 minutes

Top comments (0)