Jittator

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

Routes and prices

RouteMethodsInputOutputSats
/v1/screenshotGET, POSTurl + screenshot optionsimage, viewport only20
/v1/screenshot/fullGET, POSTurl + screenshot optionsimage, whole page30
/v1/pdfGET, POSTurl + PDF optionsPDF30
/v1/render/screenshotPOSThtml or markdown + screenshot optionsimage, viewport only20
/v1/render/screenshot/fullPOSThtml or markdown + screenshot optionsimage, whole page30
/v1/render/pdfPOSThtml or markdown + PDF optionsPDF30
/healthzGETnoneJSON statusfree
/.well-known/l402-servicesGETnoneJSON: routes, prices, mints and their feesfree

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)

NameTypeDefaultNotes
urlstringrequiredAbsolute 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.

NameTypeNotes
htmlstringAn HTML document. At most 1,048,576 bytes (1 MiB) as UTF-8. Its JavaScript runs.
markdownstringMarkdown, 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.

NameTypeDefaultRangeNotes
widthinteger1280320 to 1920Viewport width in CSS pixels.
heightinteger800240 to 1920Viewport height in CSS pixels. On the /full routes the image is as tall as the page, up to 8000 pixels.
formatstringpngpng, jpeg, webpImage format.
qualityinteger801 to 100Compression quality for jpeg and webp.
wait_msinteger00 to 10000Extra wait after the page loads, in milliseconds, for pages that draw late.

PDF options

For /v1/pdf and /v1/render/pdf.

NameTypeDefaultRangeNotes
paperstringletterletter, legal, tabloid, a3, a4, a5Paper size.
landscapebooleanfalseLandscape orientation.
print_backgroundbooleantruePrint background colours and images.
margin_innumber0.40 to 2Margin on all four sides, in inches.
mediastringprintprint, screenCSS media type to emulate. Use screen to get what a browser window shows.
wait_msinteger00 to 10000Extra 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

Limits

LimitValue
Target URL length2048 characters
Target ports80 and 443
html or markdown content1,048,576 bytes (1 MiB) as UTF-8
Whole JSON request body2,097,152 bytes (2 MiB)
Viewport320 to 1920 wide, 240 to 1920 high
Full-page screenshot height8000 pixels; taller pages are cut off and the response has X-Jittator-Truncated: true
PDF lengththe first 50 pages
Output size26,214,400 bytes (25 MiB)
Extra wait (wait_ms)10 seconds
Render time, screenshots20 seconds per job, including wait_ms
Render time, PDFs40 seconds per job (45 seconds hard limit)
ConcurrencyOne 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.

WhatLimit
Requests on /v1/ routes without a payment proof (validation and 402 challenges)20 per minute, bursts of 10
The same, all clients together60 per minute, bursts of 10
Requests carrying an L402 or Cashu proof120 per minute, bursts of 30 (a separate bucket, so unpaid traffic never uses it up)
402 challenges (new invoices) per route10 per minute
Retries and refunds after failed paid calls (below)10 per hour
/healthz, /.well-known/l402-services, unknown paths60 per minute, bursts of 20
Open connections8

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:

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.

StatusCodeMeaning
400missing_parameterurl is missing, or not exactly one of html / markdown was sent
400unknown_parameterA parameter this route does not take
400invalid_parameterWrong type or value not in the allowed list
400parameter_out_of_rangeNumber outside its range
400invalid_requestParameters sent both in the query and in a POST body
400invalid_jsonPOST body is not a JSON object
400invalid_urlURL cannot be parsed, has no host, or contains whitespace, control characters or backslashes
400url_too_longURL longer than 2048 characters
400scheme_not_allowedNot http or https
400userinfo_not_allowedURL contains a user name or password
400port_not_allowedPort other than 80 or 443
400ambiguous_ip_encodingHost written as a numeric, octal or hex IP; use dotted decimal
400 or 403host_not_allowedHost is not a fully qualified domain name (400), or is under a private or reserved suffix such as .local or .internal (403)
403target_not_publicHost resolves to a private, internal or reserved address, or (screenshots) the page redirected to one while rendering
403robots_disallowedThe site's robots.txt disallows JittatorBot for this path
404not_foundNo such route
405method_not_allowedFor example GET on a render route
413content_too_largehtml or markdown over 1 MiB
413body_too_largeRequest body over 2 MiB
415unsupported_media_typePOST body not sent as application/json
422dns_resolution_failedThe host name does not resolve
422render_rejectedThe PDF renderer refused the input
422content_navigation_blockedHTML you sent tried to navigate the page away
500internal_errorUnexpected server error
502target_http_errorThe target page answered with an HTTP error (4xx or 5xx)
502target_unreachableThe target could not be loaded: DNS failure, refused or timed-out connection, or (PDFs) a redirect to a non-public address
502output_too_largeThe result is over 25 MiB
502engine_errorThe renderer failed, for example a page that ran the browser out of memory
502engine_unavailableThe renderer is unreachable
503busyQueue full; wait Retry-After seconds and retry
504render_timeoutThe 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.

StatusCode or headerMeaning
400X-Cashu-Error headerCashu 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.
401WWW-Authenticate: L402; X-Cashu-Error for CashuProof 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.
402payment_requiredNo payment; see Payment
404not_foundAny path not listed above
429rate_limitedToo many requests; wait Retry-After seconds (Rate limits)
502validation_failedThe free check failed on the server side; no payment was requested
503validation_unavailableThe free check is unavailable; retry shortly. No payment was requested
503payment_backend_errorAn 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}}
RailChallenge (in the 402)Proof (in the retry)Status
L402 (Lightning)WWW-Authenticate: L402 macaroon="...", invoice="..."Authorization: L402 <macaroon>:<preimage>live
Cashu NUT-24X-Cashu: creqA... (NUT-18 payment request)X-Cashu: cashuB...live
MPP chargenot offered yetplanned
x402 (Lightning)not offered yetplanned

L402

  1. Send the request. Read the WWW-Authenticate header of the 402: L402 macaroon="<base64 macaroon>", invoice="<BOLT11 invoice>".
  2. 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).
  3. Send the same request again with Authorization: L402 <macaroon>:<preimage>.
$ 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)

  1. Send the request. The 402 carries an X-Cashu header with a NUT-18 payment request (creqA...). Decoded, it gives the amount a (the route's list price), the unit u (sat) and the accepted mint URLs m. That list is the authoritative one.
  2. Make a cashuB token in sats from one of the listed mints, worth the amount due (see fees below).
  3. Send the same request again with the token in the X-Cashu request 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:

MintURLInput fee (ppk)Token for a 20 / 30 sat call
Minibitshttps://mint.minibits.cash/Bitcoin (the path is case-sensitive)020 / 30 sats (list price)
21minthttps://21mint.me020 / 30 sats (list price)
Routstrhttps://mint.routstr.com020 / 30 sats (list price)
Flashhttps://forge.flashapp.me020 / 30 sats (list price)
cashu.chathttps://mint.cashu.chat020 / 30 sats (list price)
coinoshttps://mint.coinos.io100list price plus the swap fee: 21 / 31 sats in practice
macadamiahttps://mint.macadamia.cash150list 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:

ValueMeaning
allowedSend 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.
exhaustedThis proof already had its retry; a further attempt gets 401
rate_limitedYour IP address has used its 10 retries and refunds for the hour
unavailableNo 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:

ValueMeaning
refund_failedThe 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_limitedYour IP address has used its 10 retries and refunds for the hour
already_refundedA refund for this token was already issued
unavailableNo refund could be issued (internal store unavailable)

Other cases

Rules

See also: terms, acceptable use, contact and abuse reports.