API reference
Contents: Basics | Routes and prices | Parameters | How pages are rendered | Limits | Rate limits | Responses | Errors | Payment | L402 | Cashu | Failures after payment | Rules
Basics
- Base URL:
https://api.jittator.com - No account, no API key, no prepaid balance, no subscription. Every paid request carries its own payment.
- URL routes (
/v1/screenshot,/v1/screenshot/full,/v1/pdf) acceptGETwith query parameters orPOSTwith a JSON object body (Content-Type: application/json). Not both at once. - Render routes (
/v1/render/...) acceptPOSTwith a JSON object body only. - Unknown parameters are rejected with
400 unknown_parameter, so a typo never silently falls back to a default. - Every request is checked for free before payment is asked for (parameters, URL, DNS, public address, robots.txt, queue). An invalid request gets its error straight away and no invoice. POST bodies are checked too.
- In a
GETquery, percent-encode the target URL if it has its own query string, for examplecurl -G --data-urlencode "url=https://example.com/a?b=c" https://api.jittator.com/v1/screenshot. - Machine-readable prices and accepted mints:
GET https://api.jittator.com/.well-known/l402-services(free JSON).
Routes and prices
| Route | Methods | Input | Output | Sats |
|---|---|---|---|---|
/v1/screenshot | GET, POST | url + screenshot options | image, viewport only | 20 |
/v1/screenshot/full | GET, POST | url + screenshot options | image, whole page | 30 |
/v1/pdf | GET, POST | url + PDF options | 30 | |
/v1/render/screenshot | POST | html or markdown + screenshot options | image, viewport only | 20 |
/v1/render/screenshot/full | POST | html or markdown + screenshot options | image, whole page | 30 |
/v1/render/pdf | POST | html or markdown + PDF options | 30 | |
/healthz | GET | none | JSON status | free |
/.well-known/l402-services | GET | none | JSON: routes, prices, mints and their fees | free |
Prices are per call, fixed in sats. Cashu tokens from coinos or macadamia also cover that mint's swap fee (Cashu). Lightning routing fees are paid by the payer's wallet.
/healthz returns {"status": "ok", "queue": {"running": 0, "waiting": 0}}: the number of
renders running and waiting right now.
Parameters
Target (URL routes)
| Name | Type | Default | Notes |
|---|---|---|---|
url | string | required | Absolute http or https URL of a public page, at most 2048 characters.
Ports 80 and 443 only. No user:password@ part. The host must be a public domain name or a plain dotted-decimal IP
address that is public. The fragment (#...) is dropped. |
Content (render routes)
Send exactly one of these.
| Name | Type | Notes |
|---|---|---|
html | string | An HTML document. At most 1,048,576 bytes (1 MiB) as UTF-8. Its JavaScript runs. |
markdown | string | Markdown, sanitized and placed in a fixed, plain page template first. At most 1,048,576 bytes (1 MiB) as UTF-8. |
Screenshot options
For /v1/screenshot, /v1/screenshot/full, /v1/render/screenshot and /v1/render/screenshot/full.
| Name | Type | Default | Range | Notes |
|---|---|---|---|---|
width | integer | 1280 | 320 to 1920 | Viewport width in CSS pixels. |
height | integer | 800 | 240 to 1920 | Viewport height in CSS pixels. On the /full routes the image is as tall as the page, up to 8000 pixels. |
format | string | png | png, jpeg, webp | Image format. |
quality | integer | 80 | 1 to 100 | Compression quality for jpeg and webp. |
wait_ms | integer | 0 | 0 to 10000 | Extra wait after the page loads, in milliseconds, for pages that draw late. |
PDF options
For /v1/pdf and /v1/render/pdf.
| Name | Type | Default | Range | Notes |
|---|---|---|---|---|
paper | string | letter | letter, legal, tabloid, a3, a4, a5 | Paper size. |
landscape | boolean | false | Landscape orientation. | |
print_background | boolean | true | Print background colours and images. | |
margin_in | number | 0.4 | 0 to 2 | Margin on all four sides, in inches. |
media | string | print | print, screen | CSS media type to emulate. Use screen to get what a browser window shows. |
wait_ms | integer | 0 | 0 to 10000 | Extra wait after the page loads, in milliseconds. |
In a GET query, booleans are true/false (also 1/0, yes/no).
In a JSON body, use JSON booleans and numbers. Choice values (format, paper, media)
are not case-sensitive.
How pages are rendered
- All screenshots (URL, HTML and Markdown) are taken with headless Chromium driven by Playwright. All PDFs are made with Chromium's print path (Gotenberg).
- A page counts as loaded at its
loadevent;wait_msadds time after that. - A browser error page is never returned as a result. If the target cannot be reached, answers with an HTTP error, or redirects to a non-public address, you get a JSON error instead (Errors).
- Full-page screenshots grow the viewport to the page's height (at most 8000 pixels) and capture that. Elements sized
in
vhunits grow with it, so a100vhbanner can fill the image. - WebGL is turned off for screenshots, so WebGL canvases (some maps, 3D views) come out blank.
- Every request the browser makes, including images, styles, fonts, frames, scripts and redirects, is checked against the same public-address rules. Private, loopback and internal addresses are refused, and screenshots also load nothing from ports other than 80 and 443, so such resources are simply missing from the result.
- HTML you send may load public
httpandhttpsresources.file:URLs are blocked. In screenshots,data:andblob:URLs also work, and a script that navigates the page itself away fails the call with422 content_navigation_blocked. - Markdown screenshots and Markdown PDFs use different converters. They look the same, except that
~~strikethrough~~and bare URLs (not written as links) are not converted in screenshots.
Limits
| Limit | Value |
|---|---|
| Target URL length | 2048 characters |
| Target ports | 80 and 443 |
html or markdown content | 1,048,576 bytes (1 MiB) as UTF-8 |
| Whole JSON request body | 2,097,152 bytes (2 MiB) |
| Viewport | 320 to 1920 wide, 240 to 1920 high |
| Full-page screenshot height | 8000 pixels; taller pages are cut off and the response has X-Jittator-Truncated: true |
| PDF length | the first 50 pages |
| Output size | 26,214,400 bytes (25 MiB) |
Extra wait (wait_ms) | 10 seconds |
| Render time, screenshots | 20 seconds per job, including wait_ms |
| Render time, PDFs | 40 seconds per job (45 seconds hard limit) |
| Concurrency | One render at a time; at most 4 requests wait in the queue. When the queue is full, or the expected
wait is over 20 seconds, the answer is 503 busy with Retry-After. This is checked before payment too. |
Rate limits
Applied by the gateway, per client IP address unless noted. These are the current values and may change.
| What | Limit |
|---|---|
Requests on /v1/ routes without a payment proof (validation and 402 challenges) | 20 per minute, bursts of 10 |
| The same, all clients together | 60 per minute, bursts of 10 |
| Requests carrying an L402 or Cashu proof | 120 per minute, bursts of 30 (a separate bucket, so unpaid traffic never uses it up) |
| 402 challenges (new invoices) per route | 10 per minute |
| Retries and refunds after failed paid calls (below) | 10 per hour |
/healthz, /.well-known/l402-services, unknown paths | 60 per minute, bursts of 20 |
| Open connections | 8 |
Over a limit, the answer is 429 with a Retry-After header in seconds, usually with the body
{"error":{"code":"rate_limited","message":"...","retry_after_s":10}}. Wait that long before trying again.
Responses
A successful call returns 200 with the file as the body:
Content-Type:image/png,image/jpeg,image/webporapplication/pdfContent-Disposition: inline; filename="screenshot.png"(with the format's extension) orfilename="document.pdf"Cache-Control: no-storeX-Jittator-Render-Ms: time spent on the request, in millisecondsX-Jittator-Truncated: true: only on full-page screenshots cut at 8000 pixels
Results are sent only to the caller and are not stored. Save the body; it cannot be fetched again.
Errors
Errors from the renderer are JSON:
{"error": {"code": "robots_disallowed", "message": "robots.txt on 'example.com' disallows JittatorBot for this path", "status": 403}}
busy errors also carry "retry_after_s" and a Retry-After header. The same codes are
returned by the free check before payment and, for anything that only shows up while rendering, after payment.
| Status | Code | Meaning |
|---|---|---|
| 400 | missing_parameter | url is missing, or not exactly one of html / markdown was sent |
| 400 | unknown_parameter | A parameter this route does not take |
| 400 | invalid_parameter | Wrong type or value not in the allowed list |
| 400 | parameter_out_of_range | Number outside its range |
| 400 | invalid_request | Parameters sent both in the query and in a POST body |
| 400 | invalid_json | POST body is not a JSON object |
| 400 | invalid_url | URL cannot be parsed, has no host, or contains whitespace, control characters or backslashes |
| 400 | url_too_long | URL longer than 2048 characters |
| 400 | scheme_not_allowed | Not http or https |
| 400 | userinfo_not_allowed | URL contains a user name or password |
| 400 | port_not_allowed | Port other than 80 or 443 |
| 400 | ambiguous_ip_encoding | Host written as a numeric, octal or hex IP; use dotted decimal |
| 400 or 403 | host_not_allowed | Host is not a fully qualified domain name (400), or is under a private or reserved suffix such as .local or .internal (403) |
| 403 | target_not_public | Host resolves to a private, internal or reserved address, or (screenshots) the page redirected to one while rendering |
| 403 | robots_disallowed | The site's robots.txt disallows JittatorBot for this path |
| 404 | not_found | No such route |
| 405 | method_not_allowed | For example GET on a render route |
| 413 | content_too_large | html or markdown over 1 MiB |
| 413 | body_too_large | Request body over 2 MiB |
| 415 | unsupported_media_type | POST body not sent as application/json |
| 422 | dns_resolution_failed | The host name does not resolve |
| 422 | render_rejected | The PDF renderer refused the input |
| 422 | content_navigation_blocked | HTML you sent tried to navigate the page away |
| 500 | internal_error | Unexpected server error |
| 502 | target_http_error | The target page answered with an HTTP error (4xx or 5xx) |
| 502 | target_unreachable | The target could not be loaded: DNS failure, refused or timed-out connection, or (PDFs) a redirect to a non-public address |
| 502 | output_too_large | The result is over 25 MiB |
| 502 | engine_error | The renderer failed, for example a page that ran the browser out of memory |
| 502 | engine_unavailable | The renderer is unreachable |
| 503 | busy | Queue full; wait Retry-After seconds and retry |
| 504 | render_timeout | The render did not finish in time (20 seconds for screenshots, 40 for PDFs) |
Errors from the payment gateway
These come from the gateway in front of the renderer. Their JSON bodies have code and
message but no status field, and some (such as 401) are not JSON, so always read
the status code and headers.
| Status | Code or header | Meaning |
|---|---|---|
| 400 | X-Cashu-Error header | Cashu token not acceptable: mint not on the list, wrong unit, worth too little after the mint's fee, or the mint is at this gateway's custody limit (pay with another mint or Lightning). The header gives the reason. The token is not spent. |
| 401 | WWW-Authenticate: L402; X-Cashu-Error for Cashu | Proof not accepted: L402 credential invalid, already used, for another route or method, or expired; Cashu token malformed or already spent. Send the request again without a proof to get a fresh 402. |
| 402 | payment_required | No payment; see Payment |
| 404 | not_found | Any path not listed above |
| 429 | rate_limited | Too many requests; wait Retry-After seconds (Rate limits) |
| 502 | validation_failed | The free check failed on the server side; no payment was requested |
| 503 | validation_unavailable | The free check is unavailable; retry shortly. No payment was requested |
| 503 | payment_backend_error | An invoice could not be created, or a Cashu token could not be checked at its mint in time.
Retry-After: 5. A Cashu token sent with this request may already be spent: check it in your wallet before reusing it. |
Payment
Payment is handled by a gateway in front of the renderer. It first runs the free check: an invalid request gets its
error and no invoice, and a proof sent with an invalid request is not used. A valid request with no payment gets
402 Payment Required offering both rails at once. Pick one, pay, and send the same request again with
the proof of payment. Every call needs its own payment.
HTTP/2 402
www-authenticate: L402 macaroon="AGIAJEem...", invoice="lnbc200n1..."
x-cashu: creqA...
content-type: application/json
{"error":{"code":"payment_required","message":"Pay the Lightning invoice in WWW-Authenticate (L402) and retry with Authorization: L402 <macaroon>:<preimage>, or retry with a Cashu token in X-Cashu (NUT-24)","price_sat":20}}
| Rail | Challenge (in the 402) | Proof (in the retry) | Status |
|---|---|---|---|
| L402 (Lightning) | WWW-Authenticate: L402 macaroon="...", invoice="..." | Authorization: L402 <macaroon>:<preimage> | live |
| Cashu NUT-24 | X-Cashu: creqA... (NUT-18 payment request) | X-Cashu: cashuB... | live |
| MPP charge | not offered yet | planned | |
| x402 (Lightning) | not offered yet | planned | |
L402
- Send the request. Read the
WWW-Authenticateheader of the 402:L402 macaroon="<base64 macaroon>", invoice="<BOLT11 invoice>". - Check the invoice amount matches the route's price (20 or 30 sats), then pay it with any Lightning wallet. The wallet returns the payment preimage (64 hex characters).
- Send the same request again with
Authorization: L402 <macaroon>:<preimage>.
- The invoice and the macaroon expire 1 hour (3600 seconds) after the 402.
- The macaroon is bound to the route and method it was issued for: a credential bought for
GET /v1/screenshotdoes not open/v1/pdforPOST /v1/screenshot. - A credential pays for one call. Using it again gets
401, except for the one retry after a failed call described under Failures after payment.
$ curl -si "https://api.jittator.com/v1/screenshot?url=https://example.com" | grep -i www-authenticate
www-authenticate: L402 macaroon="AGIAJEem...", invoice="lnbc200n1..."
# pay lnbc200n1... with your wallet; it returns the preimage
$ curl -o shot.png \
-H "Authorization: L402 AGIAJEem...:6f1c...preimage-hex" \
"https://api.jittator.com/v1/screenshot?url=https://example.com"
With Lightning Labs' lnget, which pays from your own node and retries automatically:
lnget --max-cost 30 -o shot.png "https://api.jittator.com/v1/screenshot/full?url=https://example.com"
lnget --max-cost 30 -X POST -H "Content-Type: application/json" \
-d '{"html": "<h1>Invoice 42</h1>", "paper": "a4"}' \
-o invoice.pdf https://api.jittator.com/v1/render/pdf
Cashu (NUT-24)
- Send the request. The 402 carries an
X-Cashuheader with a NUT-18 payment request (creqA...). Decoded, it gives the amounta(the route's list price), the unitu(sat) and the accepted mint URLsm. That list is the authoritative one. - Make a
cashuBtoken in sats from one of the listed mints, worth the amount due (see fees below). - Send the same request again with the token in the
X-Cashurequest header.
$ curl -si "https://api.jittator.com/v1/pdf?url=https://example.com" | grep -i x-cashu
x-cashu: creqA...
$ curl -o page.pdf -H "X-Cashu: cashuB..." "https://api.jittator.com/v1/pdf?url=https://example.com"
The gateway swaps the token at its mint before rendering, so an accepted token is spent and cannot be used again
(401). Overpayment is kept; no change is returned.
Accepted mints and what a token must be worth:
| Mint | URL | Input fee (ppk) | Token for a 20 / 30 sat call |
|---|---|---|---|
| Minibits | https://mint.minibits.cash/Bitcoin (the path is case-sensitive) | 0 | 20 / 30 sats (list price) |
| 21mint | https://21mint.me | 0 | 20 / 30 sats (list price) |
| Routstr | https://mint.routstr.com | 0 | 20 / 30 sats (list price) |
| Flash | https://forge.flashapp.me | 0 | 20 / 30 sats (list price) |
| cashu.chat | https://mint.cashu.chat | 0 | 20 / 30 sats (list price) |
| coinos | https://mint.coinos.io | 100 | list price plus the swap fee: 21 / 31 sats in practice |
| macadamia | https://mint.macadamia.cash | 150 | list price plus the swap fee: 21 / 31 sats in practice |
The swap fee is set by the mint (NUT-02): the sum of the token's per-proof input fees in ppk, divided by 1000 and
rounded up, in sats. It depends on the number of proofs: 21 and 31 sat tokens made of the fewest proofs carry a 1 sat fee,
while a 20 sat call paid with many small proofs can need 22 sats. A token is accepted when its value minus the fee covers
the price. The payment request's a is the list price, because NUT-18 has no field for a fee; wallets that send
with "include fees" (for example cashu send -f in Nutshell, or includeFees in cashu-ts) add it
themselves.
A token that falls short is refused with 400 and an X-Cashu-Error header that says by how much,
for example:
X-Cashu-Error: Cashu token amount insufficient after the mint's 1000 msat input fee: 19000 msat (required: 20000 msat)
The token is not spent; send a larger one. The m list in the 402 and
/.well-known/l402-services (which also lists each mint's input_fee_ppk) are always current.
Failures after payment
There are no balances, so nothing is credited to an account. Instead, when a call whose payment was accepted fails in
the renderer with 422 or any 5xx (for example render_timeout,
target_unreachable, target_http_error, engine_error, busy,
content_navigation_blocked), the gateway gives back what you paid, once per payment:
L402: one retry with the same proof
The failed response carries X-L402-Retry:
| Value | Meaning |
|---|---|
allowed | Send the same Authorization: L402 <macaroon>:<preimage> again, on the same
route and method, before the macaroon expires (1 hour after the 402). It is accepted once more. You may change the
parameters, for example a larger wait_ms. |
exhausted | This proof already had its retry; a further attempt gets 401 |
rate_limited | Your IP address has used its 10 retries and refunds for the hour |
unavailable | No retry could be granted (internal store unavailable) |
Cashu: a refund token
The failed response carries X-Cashu-Refund: cashuB..., a token from the same mint worth the route's price
(not the swap fee you added). Treat it as money: redeem it into your own wallet straight away (for example
cashu receive <token>), because until you do, anyone who sees it can take it. Do not log it. You can also
pay the retry with it directly in X-Cashu; at coinos or macadamia, add the swap fee again. If no refund can be
made, the response has X-Cashu-Refund-Error instead:
| Value | Meaning |
|---|---|
refund_failed | The refund token could not be created (for example the mint did not answer). It is recorded; ask for a manual refund through the contact form. |
rate_limited | Your IP address has used its 10 retries and refunds for the hour |
already_refunded | A refund for this token was already issued |
unavailable | No refund could be issued (internal store unavailable) |
Other cases
- Errors from the free check (any
4xxor503 busybefore a 402) cost nothing and use up no proof. Fix the request; do not pay first. - A
4xxother than422after payment, such as403 target_not_publicwhen a page redirects to a private address while being captured, gets no retry or refund. 401on a retry means the proof was not accepted; do not pay again without finding out why.503 payment_backend_errormeans the payment step itself failed; wait 5 seconds and try again. Check a Cashu token in your wallet before reusing it.
Rules
- Public
httpandhttpspages only. Private, internal and reserved addresses are refused, including via DNS names or redirects that lead to them. - No logins, cookies or credentials, and no way to pass them.
- robots.txt is checked for the requested URL before the page is loaded, under the token
JittatorBot. Pages are fetched with the user agentJittatorBot/0.1 (+https://jittator.com/bot). See the bot page. - No CAPTCHA solving or anti-bot bypass. A page that blocks the bot comes back as
502 target_http_errorif the site answers with an error status, or as whatever the site shows the bot. - Results go back only to the caller and are not stored.
- Payment records (Lightning payment hash or a fingerprint of the Cashu proofs, amount, rail, mint, route and time) are kept for tax purposes; see privacy.
See also: terms, acceptable use, contact and abuse reports.