thumbto.it documentation
thumbto.it makes thumbnail images of documents: the page of a PDF you choose, ready to put in a web page or app.
Thumbnails of web pages and Office documents are coming; asking for them today returns input_not_available.
Each account gets one or more keys. A key has two parts:
- a key id like
tk_9cXk2…, which is public and appears in image URLs; - a secret like
ts_Hq4…, which signs your image URLs and authenticates API calls. Keep it on your server: never put it in a web page or an app people download.
There are two ways to get thumbnails. Image URLs go straight into an <img> tag and are made on first view.
The API lets your server ask ahead of time, check progress, and be told by webhook when a thumbnail is ready.
Image URLs
https://thumbto.it/i/{key_id}/{signature}?url=…&kind=document[&page=1&w=400&h=300&fit=cover&fmt=webp]
| Parameter | Meaning |
|---|---|
url | Required. The document's address: http or https, standard ports, on the public internet. |
kind | Required. document (today a PDF). web is coming. |
page | Which page of a document, from 1 (the default). |
w | Width in pixels, 16–1600. Default 400. |
h | Height in pixels, 16–3200. Leave it out to keep the source's proportions. |
fit | With h: cover (the default) fills w × h, cropping from the top; contain fits inside w × h without cropping. |
fmt | webp (the default), png or jpeg. |
What a request returns
- Ready: the image, with
Cache-Control, anETag, andX-Thumbto-Status: ready. Every size and format comes from one render: changingw,h,fitorfmtnever renders again. - First request: the thumbnail is queued and the request waits up to a few seconds for it. If it isn't ready by then, you get a
Preparing preview placeholder (
Cache-Control: no-store,X-Thumbto-Status: queuedorrendering) and the real image on a later load. - Failed: a No preview placeholder with the error's HTTP status and
X-Thumbto-Errorset to an error code. - A bad key, signature or parameter: a JSON error, with no image.
A document's thumbnail is kept until it hasn't been viewed for 30 days. To make a new one after the document changes, use the API's refresh.
Signing a URL
The signature is the base64url encoding (no = padding) of HMAC-SHA256 of the query string, keyed with your secret.
Sign the exact text that follows the ?, in the order you send it: any URL encoding and parameter order works, as long as the URL isn't changed after signing.
PHP
function thumbto_url(string $keyId, string $secret, array $params): string
{
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$sig = rtrim(strtr(base64_encode(hash_hmac('sha256', $query, $secret, true)), '+/', '-_'), '=');
return "https://thumbto.it/i/{$keyId}/{$sig}?{$query}";
}
echo thumbto_url('tk_…', getenv('THUMBTO_SECRET'), ['url' => 'https://example.com/spec.pdf', 'kind' => 'document', 'w' => 400]);
JavaScript (Node.js)
import { createHmac } from 'node:crypto';
function thumbtoUrl(keyId, secret, params) {
const query = new URLSearchParams(params).toString();
const sig = createHmac('sha256', secret).update(query).digest('base64url');
return `https://thumbto.it/i/${keyId}/${sig}?${query}`;
}
Python
import base64, hashlib, hmac
from urllib.parse import urlencode
def thumbto_url(key_id, secret, params):
query = urlencode(params)
sig = base64.urlsafe_b64encode(hmac.new(secret.encode(), query.encode(), hashlib.sha256).digest()).rstrip(b"=").decode()
return f"https://thumbto.it/i/{key_id}/{sig}?{query}"
Go
func thumbtoURL(keyID, secret string, params url.Values) string {
query := params.Encode()
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(query))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return "https://thumbto.it/i/" + keyID + "/" + sig + "?" + query
}
Signed URLs can be copied and reused, which is fine: they only ever show that thumbnail. Only your secret can make new ones.
API
Base https://thumbto.it/api/v1. Send Authorization: Bearer ts_… (a key with the API scope). Answers are JSON.
| Request | Does |
|---|---|
POST /thumbnails | Ask for a thumbnail: {"kind": "document", "url": "…", "page": 1, "refresh": false}. 200 if it's already made, 202 if queued. Asking again for the same thing returns the same thumbnail. refresh: true renders it again. |
GET /thumbnails/{id} | Its status: queued, rendering, ready or failed. Poll this, or use a webhook. |
GET /thumbnails/{id}/image | The image, taking w, h, fit and fmt as above. For servers that would rather fetch it than embed a URL. |
GET /usage | This month's renders, cache hits, failures and bytes served, with your limits. |
curl -s https://thumbto.it/api/v1/thumbnails \
-H "Authorization: Bearer $THUMBTO_SECRET" -H 'Content-Type: application/json' \
-d '{"kind": "document", "url": "https://example.com/spec.pdf"}'
{
"id": "Aj3o3A5u",
"kind": "document",
"url": "https://example.com/spec.pdf",
"page": 1,
"status": "ready",
"error": null,
"width": 1275, "height": 1650,
"source": {"content_type": "application/pdf", "bytes": 837376, "pages": 4, "sha256": "…"},
"requested_at": "2026-10-31T14:02:11+00:00",
"rendered_at": "2026-10-31T14:02:12+00:00",
"image_url": "https://thumbto.it/i/tk_…/…?url=…&kind=document&page=1&w=400",
"links": {"self": "…/api/v1/thumbnails/Aj3o3A5u", "image": "…/api/v1/thumbnails/Aj3o3A5u/image"}
}
image_url is a ready-signed image URL, present when the key can also sign image URLs. Fields may be added; existing ones won't change.
Webhooks
Optional: pulling always works. If your account has a webhook, thumbnails asked for through the API send
thumbnail.ready or thumbnail.failed when they finish:
POST https://your-app.example/hooks/thumbto
webhook-id: msg_Aj3o3A5u_1761919332
webhook-timestamp: 1761919333
webhook-signature: v1,K5o…=
{"type": "thumbnail.ready", "timestamp": "…", "data": { …the thumbnail, as GET /thumbnails/{id}… }}
They follow the Standard Webhooks scheme, so its libraries verify them.
By hand: base64-decode your secret after whsec_, compute HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body},
base64 it, and compare with the value after v1,. Reject timestamps more than 5 minutes old.
Answer with any 2xx. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then stop.
Webhook URLs must be HTTPS on a public address; redirects aren't followed. Use webhook-id to ignore repeats.
Errors
Errors are {"error": {"code": "…", "message": "…"}}. Program against the code; messages may be reworded. Codes are only ever added.
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 401 | The key is unknown, revoked, expired, or lacks the scope for this. |
invalid_signature | 403 | The signature does not match the URL. |
rate_limited | 429 | Too many new thumbnails requested in the last minute for this account's plan. |
input_not_available | 422 | This kind of thumbnail is not available yet. |
not_found | 404 | No such thumbnail on this account. |
account_suspended | 403 | This account is suspended. |
input_not_allowed | 403 | This account's plan does not include this kind of thumbnail. |
quota_exceeded | 429 | This account has used this month's renders. |
invalid_url | 422 | The URL is not a valid http or https address. |
invalid_options | 422 | The options are not valid. |
unresolvable_host | 422 | The URL's host name does not resolve. |
blocked_address | 422 | The URL points at a private or reserved network address. |
source_unreachable | 502 | The source could not be reached. |
source_http_error | 502 | The source answered with an error. |
source_too_large | 422 | The source is larger than this plan allows. |
source_type_unsupported | 422 | The source is not a supported type. |
source_blocked_address | 422 | The source redirected to a private or reserved network address. |
document_encrypted | 422 | The document is password protected. |
page_out_of_range | 422 | The document does not have that page. |
render_failed | 500 | The renderer could not make an image from the source. |
render_timeout | 504 | Rendering took too long. |
Plans and limits
| Plan | Renders a month | New renders a minute | Largest document |
|---|---|---|---|
| Trial | 100 | 10 | 10 MB |
| Starter | 5,000 | 30 | 25 MB |
| Business | 50,000 | 120 | 50 MB |
A render is one new thumbnail. Serving one already made — in any size or format — is not a render. Thumbnails waiting in the queue count toward the monthly limit, so it can't be overrun by asking quickly.
Our renderer
Documents are fetched from the internet by our renderers, which identify themselves as
thumbto.it-renderer/1.0 (+https://thumbto.it). They only connect to public internet addresses — never a private network —
keep no cookies, and delete every file they fetch once the thumbnail is made. Only the thumbnail image is kept.
Acceptable use
Use thumbto.it for documents you have the right to show. We remove thumbnails and block sources on a valid request from the owner of the material: write to contact@scaledapps.com with the image URL. See also the terms and privacy policy.