API reference

Generate a thumbnail in three calls.

Poke a URL, poll until it's ready, then point an <img> at it. Same flow in any language — the examples below are the whole integration.

Base URL https://webthumb.wisw.net
Auth — send your key as the X-API-Key header on POST /thumbs. Get one from your account. Serving needs no key.
Sizes960x540, 640x360, 480x270, 1200x630.

01Endpoints

MethodPathWhat it does
POST/thumbsQueue a capture. Idempotent. Returns key, status, serveUrl, statusUrl. Needs X-API-Key.
GET/thumbs/{key}/statusPoll the capture: pendinggeneratingready (or failed).
GET/t/{key}.pngThe image. Placeholder until ready, then immutable + CDN-cacheable. No key.

02Request body

FieldTypeNotes
urlstringThe page to capture. Must be public http(s).
sizestringOne of the allowed sizes. Default 960x540.
lastupdateintegerVersion token. Bump it (e.g. a unix time) to force a fresh capture; same value reuses the cached one.
priorityintegerOptional. Lower = sooner in the queue. Default 100.

03Full example — generate & get the URL

Each tab creates a thumbnail, waits for it, and prints the ready serveUrl.

# 1. Generate (queue a capture)
curl -s -X POST https://webthumb.wisw.net/thumbs \
  -H "X-API-Key: $WEBTHUMB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","size":"960x540","lastupdate":1}'
# → {"key":"cc35…","status":"pending","serveUrl":"…/t/cc35….png", …}

# 2. Poll until ready
curl -s https://webthumb.wisw.net/thumbs/cc35…/status
# → {"status":"ready","serveUrl":"…"}

# 3. Embed — no key needed
# <img src="https://webthumb.wisw.net/t/cc35….png">
import time, requests

BASE = "https://webthumb.wisw.net"
KEY  = "wt_your_key"

def thumbnail(url, size="960x540", version=1):
    r = requests.post(f"{BASE}/thumbs",
                      headers={"X-API-Key": KEY},
                      json={"url": url, "size": size, "lastupdate": version})
    r.raise_for_status()
    d = r.json()
    for _ in range(30):
        s = requests.get(f"{BASE}/thumbs/{d['key']}/status").json()
        if s["status"] == "ready":  return d["serveUrl"]
        if s["status"] == "failed": raise RuntimeError("capture failed")
        time.sleep(1)
    return d["serveUrl"]  # still pending; placeholder resolves later

print(thumbnail("https://example.com"))
// Node 18+ (built-in fetch)
const BASE = "https://webthumb.wisw.net";
const KEY  = process.env.WEBTHUMB_KEY;

async function thumbnail(url, size = "960x540", version = 1) {
  const r = await fetch(`${BASE}/thumbs`, {
    method: "POST",
    headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ url, size, lastupdate: version }),
  });
  if (!r.ok) throw new Error(`poke failed: ${r.status}`);
  const { key, serveUrl } = await r.json();
  for (let i = 0; i < 30; i++) {
    const s = await (await fetch(`${BASE}/thumbs/${key}/status`)).json();
    if (s.status === "ready")  return serveUrl;
    if (s.status === "failed") throw new Error("capture failed");
    await new Promise(res => setTimeout(res, 1000));
  }
  return serveUrl;
}

thumbnail("https://example.com").then(console.log);
<?php
$BASE = "https://webthumb.wisw.net";
$KEY  = getenv("WEBTHUMB_KEY");

function thumbnail($url, $size = "960x540", $version = 1) {
  global $BASE, $KEY;
  $ch = curl_init("$BASE/thumbs");
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["X-API-Key: $KEY", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode(["url"=>$url, "size"=>$size, "lastupdate"=>$version]),
  ]);
  $d = json_decode(curl_exec($ch), true); curl_close($ch);
  for ($i = 0; $i < 30; $i++) {
    $s = json_decode(file_get_contents("$BASE/thumbs/{$d['key']}/status"), true);
    if ($s["status"] === "ready")  return $d["serveUrl"];
    if ($s["status"] === "failed") throw new Exception("capture failed");
    sleep(1);
  }
  return $d["serveUrl"];
}

echo thumbnail("https://example.com"), "\n";
use strict; use warnings;
use LWP::UserAgent; use JSON::PP; use HTTP::Request;

my $BASE = "https://webthumb.wisw.net";
my $KEY  = $ENV{WEBTHUMB_KEY};
my $ua   = LWP::UserAgent->new;

sub thumbnail {
    my ($url, $size, $version) = @_;
    $size ||= "960x540"; $version ||= 1;
    my $req = HTTP::Request->new(POST => "$BASE/thumbs");
    $req->header("X-API-Key" => $KEY, "Content-Type" => "application/json");
    $req->content(encode_json({ url => $url, size => $size, lastupdate => $version }));
    my $d = decode_json($ua->request($req)->decoded_content);
    for (1..30) {
        my $s = decode_json($ua->get("$BASE/thumbs/$d->{key}/status")->decoded_content);
        return $d->{serveUrl} if $s->{status} eq "ready";
        die "capture failed" if $s->{status} eq "failed";
        sleep 1;
    }
    return $d->{serveUrl};
}

print thumbnail("https://example.com"), "\n";
require "net/http"; require "json"; require "uri"

BASE = "https://webthumb.wisw.net"
KEY  = ENV["WEBTHUMB_KEY"]

def thumbnail(url, size: "960x540", version: 1)
  res = Net::HTTP.post(URI("#{BASE}/thumbs"),
    { url: url, size: size, lastupdate: version }.to_json,
    "X-API-Key" => KEY, "Content-Type" => "application/json")
  d = JSON.parse(res.body)
  30.times do
    s = JSON.parse(Net::HTTP.get(URI("#{BASE}/thumbs/#{d['key']}/status")))
    return d["serveUrl"] if s["status"] == "ready"
    raise "capture failed" if s["status"] == "failed"
    sleep 1
  end
  d["serveUrl"]
end

puts thumbnail("https://example.com")
package main

import ("bytes"; "encoding/json"; "fmt"; "net/http"; "time")

const base = "https://webthumb.wisw.net"
var key = "wt_your_key"

func thumbnail(url, size string, version int) (string, error) {
    body, _ := json.Marshal(map[string]any{"url": url, "size": size, "lastupdate": version})
    req, _ := http.NewRequest("POST", base+"/thumbs", bytes.NewReader(body))
    req.Header.Set("X-API-Key", key)
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil { return "", err }
    var d struct{ Key, ServeUrl string }
    json.NewDecoder(res.Body).Decode(&d); res.Body.Close()
    for i := 0; i < 30; i++ {
        s, _ := http.Get(base + "/thumbs/" + d.Key + "/status")
        var st struct{ Status string }
        json.NewDecoder(s.Body).Decode(&st); s.Body.Close()
        if st.Status == "ready"  { return d.ServeUrl, nil }
        if st.Status == "failed" { return "", fmt.Errorf("capture failed") }
        time.Sleep(time.Second)
    }
    return d.ServeUrl, nil
}

func main() { u, _ := thumbnail("https://example.com", "960x540", 1); fmt.Println(u) }
using System.Net.Http.Json;
using System.Text.Json;

var BASE = "https://webthumb.wisw.net";
var KEY  = Environment.GetEnvironmentVariable("WEBTHUMB_KEY");
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-API-Key", KEY);

async Task<string> Thumbnail(string url, string size = "960x540", int version = 1) {
    var res  = await http.PostAsJsonAsync($"{BASE}/thumbs", new { url, size, lastupdate = version });
    var d    = await res.Content.ReadFromJsonAsync<JsonElement>();
    var key  = d.GetProperty("key").GetString();
    var serve = d.GetProperty("serveUrl").GetString();
    for (int i = 0; i < 30; i++) {
        var s = await http.GetFromJsonAsync<JsonElement>($"{BASE}/thumbs/{key}/status");
        var status = s.GetProperty("status").GetString();
        if (status == "ready")  return serve;
        if (status == "failed") throw new Exception("capture failed");
        await Task.Delay(1000);
    }
    return serve;
}

Console.WriteLine(await Thumbnail("https://example.com"));

04Serving in your UI

Once you have a serveUrl, it's just an image. It shows a placeholder until the capture is ready, then the real PNG — cached and unlimited.

<img src="https://webthumb.wisw.net/t/cc35….png" alt="preview">
Tip — poke on save (fire-and-forget), store the returned serveUrl on your record, and render the <img> immediately. To make a tile fill in live, poll /thumbs/{key}/status and swap img.src when it turns ready. Need Java, Rust, Elixir, or an OpenAPI spec? Ask us.