---
name: jittator
description: Screenshot or PDF a public web page, or render HTML or Markdown to an image or PDF, with the Jittator API (https://api.jittator.com), paying 20 or 30 sats per call over Lightning (L402) or Cashu ecash. Use when you need a picture or PDF of a page and cannot render it yourself. No account or API key.
---

# Jittator: screenshots and PDFs, paid per call

Base URL: `https://api.jittator.com`. Every paid request carries its own payment. There is no account, key, balance or
subscription. Full reference: https://jittator.com/docs . OpenAPI: https://jittator.com/openapi.yaml . Prices and
mints as JSON: https://api.jittator.com/.well-known/l402-services (free).

## When to use it

- You need an image or PDF of a public web page, as a browser would show it.
- You have HTML or Markdown and need it as a PDF or image.

Do not use it for pages behind a login, pages needing cookies, private or internal addresses (localhost, 10.x, 192.168.x,
intranet names), or to get past CAPTCHAs or bot blocks. Those requests are refused or fail. If you only need the text of a
page, fetching it yourself is cheaper.

## Spending

Each call costs real money. Spend only within the budget your user gave you. If you have no budget, ask before paying.
Check that the amount in the 402 matches the price below before you pay.

| Route | Method | Sats |
|---|---|---|
| `/v1/screenshot` | GET or POST | 20 |
| `/v1/screenshot/full` | GET or POST | 30 |
| `/v1/pdf` | GET or POST | 30 |
| `/v1/render/screenshot` | POST | 20 |
| `/v1/render/screenshot/full` | POST | 30 |
| `/v1/render/pdf` | POST | 30 |
| `/healthz` | GET | free |
| `/.well-known/l402-services` | GET | free |

Cashu tokens from coinos or macadamia must also cover that mint's swap fee (21 or 31 sats in practice). Lightning routing
fees are paid by your wallet.

## Step 1: build a valid request

Every request is checked for free before you are asked to pay. An invalid one gets its `4xx` error (or `503 busy`)
instead of a 402, creates no invoice and uses up no proof. Checking these yourself saves round trips:

- URL routes: `url` is an absolute `http://` or `https://` URL of a public page, at most 2048 characters, port 80 or
  443, no `user:pass@`.
- Render routes: exactly one of `html` or `markdown`, at most 1 MiB as UTF-8, sent as a JSON body with
  `Content-Type: application/json` (whole body at most 2 MiB).
- Only these other parameters are accepted (anything else is `400 unknown_parameter`):
  - screenshots: `width` (320-1920, default 1280), `height` (240-1920, default 800), `format` (`png`, `jpeg`, `webp`;
    default `png`), `quality` (1-100, default 80, for jpeg and webp), `wait_ms` (0-10000, default 0)
  - PDFs: `paper` (`letter`, `legal`, `tabloid`, `a3`, `a4`, `a5`; default `letter`), `landscape` (default false),
    `print_background` (default true), `margin_in` (0-2 inches, default 0.4), `media` (`print` or `screen`, default
    `print`), `wait_ms` (0-10000, default 0)
- Optional: `GET https://api.jittator.com/healthz` (free). A long `queue.waiting` means you may get `503 busy`.

For GET, percent-encode the target URL in the query (`curl -G --data-urlencode "url=..."`). For POST, send a JSON object
and no query string.

## Step 2: send the request and read the 402

```
GET /v1/screenshot?url=https%3A%2F%2Fexample.com HTTP/1.1
Host: api.jittator.com
```

A valid request without payment gets `402 Payment Required` with two offers. Use one:

```
HTTP/2 402
www-authenticate: L402 macaroon="AGIAJEem...", invoice="lnbc200n1..."
x-cashu: creqA...
content-type: application/json

{"error":{"code":"payment_required","message":"...","price_sat":20}}
```

- `WWW-Authenticate` is the L402 offer: a macaroon and a BOLT11 Lightning invoice. Both expire 1 hour after the 402.
- `X-Cashu` is the Cashu NUT-24 offer: a NUT-18 (`creqA`) payment request. Decoded it has `a` (amount, the list
  price), `u` (unit, `sat`) and `m` (accepted mint URLs).

If you have a Lightning wallet, use L402. If you hold Cashu ecash at one of the listed mints, use Cashu.

## Step 3a: pay with L402

1. Check the invoice amount (20 or 30 sats for this route).
2. Pay the invoice with your Lightning wallet. Keep the preimage it returns (64 hex characters).
3. Within the hour, send the same request again (same route and method) with:

```
Authorization: L402 <macaroon>:<preimage>
```

The credential pays for one call; using it again gets `401` (except for the retry in step 4).

If you have Lightning Labs' `lnget` with a configured node, it does steps 1 to 3 for you:

```
lnget --max-cost 30 -o page.png "https://api.jittator.com/v1/screenshot/full?url=https://example.com"
```

## Step 3b: pay with Cashu

1. Decode the `X-Cashu` payment request. Check the amount and that you hold sats at one of the mints in `m`:
   - list price: `https://mint.minibits.cash/Bitcoin`, `https://21mint.me`, `https://mint.routstr.com`,
     `https://forge.flashapp.me`, `https://mint.cashu.chat`
   - list price plus the mint's swap fee: `https://mint.coinos.io` (100 ppk), `https://mint.macadamia.cash` (150 ppk)
2. Make a `cashuB` token in sats from that mint. At coinos or macadamia the token's value minus the mint's swap fee
   (NUT-02: ceil(sum of input_fee_ppk over its proofs / 1000) sats) must still cover the price: send with "include
   fees" (`cashu send -f 20`, cashu-ts `includeFees`), which gives 21 or 31 sats in practice.
3. Send exactly the same request again with:

```
X-Cashu: cashuB...
```

The token is swapped at its mint before rendering, so once accepted it is spent. Overpayment is not returned.

## Step 4: handle the answer

- `200`: the body is the file. `Content-Type` is `image/png`, `image/jpeg`, `image/webp` or `application/pdf`. Save it now:
  results are not stored and cannot be fetched again. `X-Jittator-Truncated: true` means a full-page screenshot was cut
  at 8000 pixels.
- `400` with an `X-Cashu-Error` header: your Cashu token was refused (mint not listed, too little after the mint's fee,
  or the mint is at this gateway's custody limit). The header says why; the token is not spent. Fix it and send again.
- `401`: your proof was not accepted (invalid, already used, other route or method, expired, or a spent Cashu token;
  `X-Cashu-Error` explains Cashu cases). Do not pay a second time without finding out why. Sending the request with no
  proof gets a fresh 402.
- `400`, `403`, `404`, `405`, `413`, `415`, `422` with a JSON `error.code` before any payment: the request cannot succeed
  as sent. Fix it, and do not retry it unchanged. `403 robots_disallowed` and `403 target_not_public` mean the page cannot
  be fetched by this service at all.
- `429 rate_limited`: wait `Retry-After` seconds.
- `503 busy` before payment: wait `Retry-After` seconds, then send the request again. Nothing was charged.
- `503 payment_backend_error`: the payment step failed on the server; wait 5 seconds and retry. If you sent a Cashu token,
  check in your wallet whether it was spent before reusing it.
- A paid call that fails while rendering (`422` or `5xx`, for example `504 render_timeout`, `502 target_unreachable`,
  `502 target_http_error`, `503 busy`) is made good once:
  - L402: the response has `X-L402-Retry`. `allowed` means the same `Authorization` header works once more on the same
    route and method within the hour (you may change parameters, for example a larger `wait_ms`). `exhausted` means this
    proof already had its retry; `rate_limited` means your IP used its 10 retries and refunds this hour.
  - Cashu: the response has `X-Cashu-Refund: cashuB...`, a token for the price from the same mint. It is money: redeem it
    into your wallet at once (`cashu receive <token>`) or use it to pay the retry, and never log or share it. If instead
    there is `X-Cashu-Refund-Error: refund_failed`, ask for a manual refund at https://jittator.com/contact .
  - A `403` after payment (for example a page that redirects to a private address while being captured) is not retried or
    refunded.

Error bodies look like:

```
{"error": {"code": "parameter_out_of_range", "message": "'width' must be between 320 and 1920", "status": 400}}
```

Gateway errors (`429`, `502 validation_failed`, `503 payment_backend_error`) have `code` and `message` but no `status`.

## Examples

Viewport screenshot of a URL, paid with L402 using curl:

```
curl -si "https://api.jittator.com/v1/screenshot?url=https://example.com&width=1024&format=webp" | grep -i '^www-authenticate'
# pay the invoice, then:
curl -o shot.webp -H "Authorization: L402 $MACAROON:$PREIMAGE" \
  "https://api.jittator.com/v1/screenshot?url=https://example.com&width=1024&format=webp"
```

URL to A4 PDF as a POST, paid with Cashu:

```
curl -si -X POST https://api.jittator.com/v1/pdf -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "paper": "a4", "media": "screen"}' | grep -i '^x-cashu'
# make a 30 sat cashuB token from a listed mint, then:
curl -o page.pdf -D headers.txt -X POST https://api.jittator.com/v1/pdf -H "Content-Type: application/json" \
  -H "X-Cashu: $TOKEN" \
  -d '{"url": "https://example.com", "paper": "a4", "media": "screen"}'
# if it failed, headers.txt may hold X-Cashu-Refund: redeem it, then delete headers.txt
```

Markdown to PDF:

```
curl -o notes.pdf -X POST https://api.jittator.com/v1/render/pdf -H "Content-Type: application/json" \
  -H "Authorization: L402 $MACAROON:$PREIMAGE" \
  -d '{"markdown": "# Meeting notes\n\n- item one\n- item two"}'
```

Handling the 402 in code (the wallet calls are yours to supply):

```python
import time

import httpx

def call(method, path, **kw):
    url = "https://api.jittator.com" + path
    extra = kw.pop("headers", {})
    r = httpx.request(method, url, headers=extra, timeout=90, **kw)
    if r.status_code != 402:
        return r                                    # 4xx/503 here cost nothing
    challenge = r.headers.get("www-authenticate", "")
    if challenge.startswith("L402 "):
        macaroon, invoice = parse_l402(challenge)   # macaroon="...", invoice="..."
        check_amount(invoice)                       # 20 or 30 sats, within budget
        preimage = wallet_pay(invoice)              # hex preimage from your wallet
        proof = {"Authorization": f"L402 {macaroon}:{preimage}"}
    else:
        request = r.headers["x-cashu"]              # creqA...
        token = cashu_wallet_send(request)          # cashuB... from a mint in the request, fees included
        proof = {"X-Cashu": token}
    r = httpx.request(method, url, headers={**extra, **proof}, timeout=90, **kw)
    if "x-cashu-refund" in r.headers:
        cashu_wallet_receive(r.headers["x-cashu-refund"])   # redeem at once; never log it
    elif r.headers.get("x-l402-retry") == "allowed":
        time.sleep(int(r.headers.get("retry-after", "0")))  # set on 503 busy
        r = httpx.request(method, url, headers={**extra, **proof}, timeout=90, **kw)
    return r
```

## Rules

Public pages only; robots.txt is honoured (user agent `JittatorBot/0.1 (+https://jittator.com/bot)`); no logins, cookies
or credentials; no anti-bot bypass; results go only to you and are not stored. Terms: https://jittator.com/terms ,
acceptable use: https://jittator.com/acceptable-use , privacy: https://jittator.com/privacy , contact:
https://jittator.com/contact .
