API documentation

A single endpoint, explicit parameters. Authentication by API key (token).

Language:

Endpoint

GET https://webshotninja.com/api?token=YOUR_API_KEY&url=https://example.com

Add your API key as the token parameter. You'll find it in your dashboard.

Parameters

ParameterDefaultDescription
tokenYour API key (required).
urlThe URL of the page to capture, URL-encoded (required).
width1600Browser width in pixels (100–3840).
height1200Browser height in pixels (100–4320).
full01 = capture the entire page height.
formatwebppng, jpeg or webp.
quality80jpeg/webp quality (1–100).
delay0Wait in milliseconds after page load (max 10000). Useful for sites with intro animations.
nocookies11 = dismiss/hide cookie banners (default). 0 = capture the page as-is.
scrollauto1 = force lazy-loading auto-scroll, 0 = disable. Default: automatic when full=1.
nocache01 = force a fresh capture (skip the cache). Paid plans only.
selectorCSS selector to capture a specific element (e.g. #hero, .pricing-table). The element is awaited up to 10s. Returns 404 if not found. Overrides full if both are set.
noads01 = block ads and trackers (powered by EasyList). Can be combined with nocookies.
cssCustom CSS injected into the page before capture (max 10 KB). API only, not available on the demo.
jsCustom JavaScript executed in the page context before capture (max 10 KB). API only. If the script throws, the capture still completes and the error is returned in the X-WSN-JS-Error header.
dark01 = enable dark mode (prefers-color-scheme: dark). Only works if the target site supports native dark mode.
deviceDevice preset: iphone14, iphone14pro, pixel7, ipad, galaxys23. Sets viewport, user-agent and touch emulation. Explicit width/height override the device viewport.
scale1Device scale factor (1, 2 or 3). scale=2 produces a retina image (2x resolution, heavier file).

Code examples

Basic screenshot

curl "https://webshotninja.com/api?token=YOUR_API_KEY&url=https%3A%2F%2Fexample.com&full=1&format=webp" \
  -o screenshot.webp
const params = new URLSearchParams({
  token: 'YOUR_API_KEY',
  url: 'https://example.com',
  full: '1',
  format: 'webp',
});
const res = await fetch(`https://webshotninja.com/api?${params}`);
const fs = require('fs');
fs.writeFileSync('screenshot.webp', Buffer.from(await res.arrayBuffer()));
import requests

r = requests.get('https://webshotninja.com/api', params={
    'token': 'YOUR_API_KEY',
    'url': 'https://example.com',
    'full': '1',
    'format': 'webp',
})
with open('screenshot.webp', 'wb') as f:
    f.write(r.content)
$params = http_build_query([
    'token'  => 'YOUR_API_KEY',
    'url'    => 'https://example.com',
    'full'   => '1',
    'format' => 'webp',
]);
file_put_contents('screenshot.webp', file_get_contents('https://webshotninja.com/api?' . $params));
require 'net/http'
require 'uri'

uri = URI('https://webshotninja.com/api')
uri.query = URI.encode_www_form(
  token: 'YOUR_API_KEY',
  url: 'https://example.com',
  full: '1',
  format: 'webp'
)
File.binwrite('screenshot.webp', Net::HTTP.get(uri))
package main

import (
    "io"
    "net/http"
    "net/url"
    "os"
)

func main() {
    params := url.Values{
        "token":  {"YOUR_API_KEY"},
        "url":    {"https://example.com"},
        "full":   {"1"},
        "format": {"webp"},
    }
    resp, _ := http.Get("https://webshotninja.com/api?" + params.Encode())
    defer resp.Body.Close()
    f, _ := os.Create("screenshot.webp")
    io.Copy(f, resp.Body)
}

Mobile device + dark mode

curl "https://webshotninja.com/api?token=YOUR_API_KEY&url=https%3A%2F%2Fexample.com&device=iphone14&dark=1&scale=2" \
  -o mobile-dark.webp
const res = await fetch(`https://webshotninja.com/api?` + new URLSearchParams({
  token: 'YOUR_API_KEY',
  url: 'https://example.com',
  device: 'iphone14',
  dark: '1',
  scale: '2',
}));
fs.writeFileSync('mobile-dark.webp', Buffer.from(await res.arrayBuffer()));
r = requests.get('https://webshotninja.com/api', params={
    'token': 'YOUR_API_KEY',
    'url': 'https://example.com',
    'device': 'iphone14',
    'dark': '1',
    'scale': '2',
})
with open('mobile-dark.webp', 'wb') as f:
    f.write(r.content)
$params = http_build_query([
    'token'  => 'YOUR_API_KEY',
    'url'    => 'https://example.com',
    'device' => 'iphone14',
    'dark'   => '1',
    'scale'  => '2',
]);
file_put_contents('mobile-dark.webp', file_get_contents('https://webshotninja.com/api?' . $params));
uri = URI('https://webshotninja.com/api')
uri.query = URI.encode_www_form(token: 'YOUR_API_KEY', url: 'https://example.com',
  device: 'iphone14', dark: '1', scale: '2')
File.binwrite('mobile-dark.webp', Net::HTTP.get(uri))
params := url.Values{"token": {"YOUR_API_KEY"}, "url": {"https://example.com"},
    "device": {"iphone14"}, "dark": {"1"}, "scale": {"2"}}
resp, _ := http.Get("https://webshotninja.com/api?" + params.Encode())
defer resp.Body.Close()
f, _ := os.Create("mobile-dark.webp")
io.Copy(f, resp.Body)

CSS selector capture

curl "https://webshotninja.com/api?token=YOUR_API_KEY&url=https%3A%2F%2Fexample.com&selector=%23hero&format=png" \
  -o hero.png
const res = await fetch(`https://webshotninja.com/api?` + new URLSearchParams({
  token: 'YOUR_API_KEY',
  url: 'https://example.com',
  selector: '#hero',
  format: 'png',
}));
fs.writeFileSync('hero.png', Buffer.from(await res.arrayBuffer()));
r = requests.get('https://webshotninja.com/api', params={
    'token': 'YOUR_API_KEY',
    'url': 'https://example.com',
    'selector': '#hero',
    'format': 'png',
})
with open('hero.png', 'wb') as f:
    f.write(r.content)
$params = http_build_query([
    'token'    => 'YOUR_API_KEY',
    'url'      => 'https://example.com',
    'selector' => '#hero',
    'format'   => 'png',
]);
file_put_contents('hero.png', file_get_contents('https://webshotninja.com/api?' . $params));
uri = URI('https://webshotninja.com/api')
uri.query = URI.encode_www_form(token: 'YOUR_API_KEY', url: 'https://example.com',
  selector: '#hero', format: 'png')
File.binwrite('hero.png', Net::HTTP.get(uri))
params := url.Values{"token": {"YOUR_API_KEY"}, "url": {"https://example.com"},
    "selector": {"#hero"}, "format": {"png"}}
resp, _ := http.Get("https://webshotninja.com/api?" + params.Encode())
defer resp.Body.Close()
f, _ := os.Create("hero.png")
io.Copy(f, resp.Body)

Signed URLs

Embed capture URLs in your frontend (e.g. img src) without exposing your API key. Sign the sorted query string (all params except sig) with HMAC-SHA256 using your signing secret. Add user (your user ID), expires (Unix timestamp) and sig (the hex signature). The capture is served and counted against your quota. Your signing secret is in your dashboard.

GET https://webshotninja.com/api?url=https://example.com&user=USER_ID&expires=TIMESTAMP&sig=HMAC_HEX
# Generate signed URL with openssl
PARAMS="expires=$(( $(date +%s) + 3600 ))&url=https%3A%2F%2Fexample.com&user=YOUR_USER_ID"
SIG=$(echo -n "$PARAMS" | openssl dgst -sha256 -hmac "YOUR_SIGNING_SECRET" | cut -d' ' -f2)
curl "https://webshotninja.com/api?${PARAMS}&sig=${SIG}" -o signed.webp
const crypto = require('crypto');
const params = new URLSearchParams({
  url: 'https://example.com',
  user: 'YOUR_USER_ID',
  expires: String(Math.floor(Date.now() / 1000) + 3600),
});
const sorted = [...params].sort((a, b) => a[0].localeCompare(b[0]))
  .map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
const sig = crypto.createHmac('sha256', 'YOUR_SIGNING_SECRET')
  .update(sorted).digest('hex');
const url = `https://webshotninja.com/api?${sorted}&sig=${sig}`;
// Use in <img src> — no API key exposed
import hmac, hashlib, time
from urllib.parse import urlencode

params = sorted({
    'url': 'https://example.com',
    'user': 'YOUR_USER_ID',
    'expires': str(int(time.time()) + 3600),
}.items())
qs = urlencode(params)
sig = hmac.new(b'YOUR_SIGNING_SECRET', qs.encode(), hashlib.sha256).hexdigest()
url = f'https://webshotninja.com/api?{qs}&sig={sig}'
$params = [
    'url'     => 'https://example.com',
    'user'    => 'YOUR_USER_ID',
    'expires' => (string)(time() + 3600),
];
ksort($params);
$qs = implode('&', array_map(
    fn($k, $v) => rawurlencode($k) . '=' . rawurlencode($v),
    array_keys($params), $params
));
$sig = hash_hmac('sha256', $qs, 'YOUR_SIGNING_SECRET');
$url = 'https://webshotninja.com/api?' . $qs . '&sig=' . $sig;
require 'openssl'
require 'uri'

params = {
  'expires' => (Time.now.to_i + 3600).to_s,
  'url'     => 'https://example.com',
  'user'    => 'YOUR_USER_ID',
}.sort.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }.join('&')
sig = OpenSSL::HMAC.hexdigest('sha256', 'YOUR_SIGNING_SECRET', params)
url = "https://webshotninja.com/api?#{params}&sig=#{sig}"
import (
    "crypto/hmac"
    "crypto/sha256"
    "fmt"
    "net/url"
    "sort"
    "time"
)

params := url.Values{
    "url":     {"https://example.com"},
    "user":    {"YOUR_USER_ID"},
    "expires": {fmt.Sprint(time.Now().Unix() + 3600)},
}
// Sort and encode
keys := make([]string, 0)
for k := range params { keys = append(keys, k) }
sort.Strings(keys)
// ... build sorted query string, HMAC-SHA256 sign

HTML rendering

POST /api/render — render raw HTML to an image. Send a JSON body with a html field (max 2 MB) and your token. All screenshot parameters (width, height, format, css, js, selector, dark, device, scale…) are accepted. Ideal for generating Open Graph / social card images. API only, counts toward your quota.

POST https://webshotninja.com/api/render
Content-Type: application/json
curl -X POST https://webshotninja.com/api/render \
  -H "Content-Type: application/json" \
  -d '{"token":"YOUR_API_KEY","html":"<h1>Hello</h1><p>Social card</p>","width":1200,"height":630,"format":"png"}' \
  -o card.png
const res = await fetch('https://webshotninja.com/api/render', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    token: 'YOUR_API_KEY',
    html: '<h1>Hello</h1><p>Social card</p>',
    width: 1200, height: 630, format: 'png',
  }),
});
fs.writeFileSync('card.png', Buffer.from(await res.arrayBuffer()));
r = requests.post('https://webshotninja.com/api/render', json={
    'token': 'YOUR_API_KEY',
    'html': '<h1>Hello</h1><p>Social card</p>',
    'width': 1200, 'height': 630, 'format': 'png',
})
with open('card.png', 'wb') as f:
    f.write(r.content)
$ch = curl_init('https://webshotninja.com/api/render');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'token'  => 'YOUR_API_KEY',
        'html'   => '<h1>Hello</h1><p>Social card</p>',
        'width'  => 1200, 'height' => 630, 'format' => 'png',
    ]),
]);
file_put_contents('card.png', curl_exec($ch));
require 'net/http'
require 'json'

uri = URI('https://webshotninja.com/api/render')
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = { token: 'YOUR_API_KEY', html: '<h1>Hello</h1>',
             width: 1200, height: 630, format: 'png' }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
File.binwrite('card.png', res.body)
body, _ := json.Marshal(map[string]interface{}{
    "token": "YOUR_API_KEY",
    "html":  "<h1>Hello</h1>",
    "width": 1200, "height": 630, "format": "png",
})
resp, _ := http.Post("https://webshotninja.com/api/render", "application/json", bytes.NewReader(body))
defer resp.Body.Close()
f, _ := os.Create("card.png")
io.Copy(f, resp.Body)

Queue status

Check the current capture queue before sending a request.

GET https://webshotninja.com/api/queue
// Response
{ "running": 1, "queued": 3, "maxConcurrent": 2 }

Every API response also includes these headers:

HeaderDescription
X-Queue-SizeRequests waiting in queue
X-Queue-RunningCaptures currently in progress
X-CacheHIT (cached) or MISS (fresh capture)
X-Quota-UsedCaptures used this month
X-Quota-LimitMonthly quota limit
X-Render-TimeCapture duration (e.g. 3200ms)

Response

The raw image (Content-Type image/png, image/jpeg or image/webp). On error, a JSON object with an error field. The X-Cache header reads HIT (cached) or MISS (fresh capture).