openapi: 3.1.0 info: title: Jittator API version: "0.1.0" summary: Pay-per-call screenshots and PDFs for AI agents, paid in bitcoin. description: | Screenshots and PDFs of public web pages, or of HTML or Markdown you send. Every paid request carries its own payment; there is no account, API key, prepaid balance or subscription. Every request is first checked for free (parameters, URL, DNS, public address, robots.txt, queue). An invalid request gets its 4xx error (or 503 busy) and no invoice, and a proof sent with it is not used. A valid unpaid request gets HTTP 402 with two offers: an L402 challenge in `WWW-Authenticate` and a Cashu NUT-24 payment request (NUT-18 `creqA...`) in `X-Cashu`. Pay one and send the same request again with `Authorization: L402 :` or `X-Cashu: cashuB...`. If a paid call then fails with 422 or 5xx, the response carries `X-L402-Retry` (L402: `allowed` means the same proof is accepted once more on the same route and method within the macaroon's hour) or `X-Cashu-Refund` (Cashu: a refund token for the price from the same mint; redeem it at once) or `X-Cashu-Refund-Error`. Prices in sats: x-price-sats on each operation; also served as JSON at /.well-known/l402-services. MPP charge and x402 over Lightning are planned. Reference: https://jittator.com/docs termsOfService: https://jittator.com/terms contact: name: Jittator contact form url: https://jittator.com/contact externalDocs: description: API reference url: https://jittator.com/docs servers: - url: https://api.jittator.com security: - L402: [] - Cashu: [] tags: - name: url description: Render a public URL - name: render description: Render HTML or Markdown you send - name: status paths: /v1/screenshot: get: tags: [url] operationId: screenshotUrlGet summary: Viewport screenshot of a public URL x-price-sats: 20 parameters: - $ref: "#/components/parameters/Url" - $ref: "#/components/parameters/Width" - $ref: "#/components/parameters/Height" - $ref: "#/components/parameters/Format" - $ref: "#/components/parameters/Quality" - $ref: "#/components/parameters/WaitMs" responses: "200": { $ref: "#/components/responses/Image" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } post: tags: [url] operationId: screenshotUrlPost summary: Viewport screenshot of a public URL (JSON body) x-price-sats: 20 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UrlScreenshotRequest" } responses: "200": { $ref: "#/components/responses/Image" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } /v1/screenshot/full: get: tags: [url] operationId: screenshotFullUrlGet summary: Full-page screenshot of a public URL description: The image is as tall as the page, up to 8000 pixels; taller pages are cut and marked with X-Jittator-Truncated. x-price-sats: 30 parameters: - $ref: "#/components/parameters/Url" - $ref: "#/components/parameters/Width" - $ref: "#/components/parameters/Height" - $ref: "#/components/parameters/Format" - $ref: "#/components/parameters/Quality" - $ref: "#/components/parameters/WaitMs" responses: "200": { $ref: "#/components/responses/Image" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } post: tags: [url] operationId: screenshotFullUrlPost summary: Full-page screenshot of a public URL (JSON body) x-price-sats: 30 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UrlScreenshotRequest" } responses: "200": { $ref: "#/components/responses/Image" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } /v1/pdf: get: tags: [url] operationId: pdfUrlGet summary: Public URL to PDF description: The first 50 pages are returned. x-price-sats: 30 parameters: - $ref: "#/components/parameters/Url" - $ref: "#/components/parameters/Paper" - $ref: "#/components/parameters/Landscape" - $ref: "#/components/parameters/PrintBackground" - $ref: "#/components/parameters/MarginIn" - $ref: "#/components/parameters/Media" - $ref: "#/components/parameters/WaitMs" responses: "200": { $ref: "#/components/responses/Pdf" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } post: tags: [url] operationId: pdfUrlPost summary: Public URL to PDF (JSON body) x-price-sats: 30 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UrlPdfRequest" } responses: "200": { $ref: "#/components/responses/Pdf" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } /v1/render/screenshot: post: tags: [render] operationId: renderScreenshot summary: Viewport screenshot of HTML or Markdown you send x-price-sats: 20 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ContentScreenshotRequest" } responses: "200": { $ref: "#/components/responses/Image" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } /v1/render/screenshot/full: post: tags: [render] operationId: renderScreenshotFull summary: Full-page screenshot of HTML or Markdown you send x-price-sats: 30 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ContentScreenshotRequest" } responses: "200": { $ref: "#/components/responses/Image" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } /v1/render/pdf: post: tags: [render] operationId: renderPdf summary: HTML or Markdown you send, to PDF x-price-sats: 30 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ContentPdfRequest" } responses: "200": { $ref: "#/components/responses/Pdf" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "429": { $ref: "#/components/responses/TooManyRequests" } default: { $ref: "#/components/responses/Error" } /healthz: get: tags: [status] operationId: healthz summary: Service status (free) x-price-sats: 0 security: [] responses: "200": description: Service is up content: application/json: schema: type: object required: [status, queue] properties: status: { type: string, const: ok } queue: type: object properties: running: { type: integer, minimum: 0, description: Renders running now } waiting: { type: integer, minimum: 0, description: Renders waiting now } "429": { $ref: "#/components/responses/TooManyRequests" } /.well-known/l402-services: get: tags: [status] operationId: l402Services summary: Routes, prices and accepted payment methods and mints (free) x-price-sats: 0 security: [] responses: "200": description: Discovery document content: application/json: schema: type: object properties: version: { type: string } service: { type: object } payment_methods: type: array description: L402 and Cashu NUT-24; the Cashu entry lists each mint's url and input_fee_ppk, and the fee rule. items: { type: object } routes: type: array items: type: object properties: path: { type: string } methods: { type: array, items: { type: string } } price: type: object properties: amount_sat: { type: integer } type: { type: string } macaroon_timeout_secs: { type: integer } "429": { $ref: "#/components/responses/TooManyRequests" } components: securitySchemes: L402: type: http scheme: L402 description: | Lightning. The 402 response carries `WWW-Authenticate: L402 macaroon="", invoice=""`. Pay the invoice and send `Authorization: L402 :` (preimage as hex). Invoice and macaroon expire 3600 s after the 402. The macaroon is bound to the route and method and pays one call; reuse gets 401 (except one retry after a failed paid call, signalled by `X-L402-Retry: allowed`). Cashu: type: apiKey in: header name: X-Cashu description: | Cashu NUT-24 (https://github.com/cashubtc/nuts/blob/main/24.md). The 402 response carries an `X-Cashu` NUT-18 payment request (`creqA...`) with amount `a` (the list price), unit `u` (sat) and accepted mints `m`. Send a `cashuB...` token from one of those mints in the `X-Cashu` request header. At list price: https://mint.minibits.cash/Bitcoin, https://21mint.me, https://mint.routstr.com, https://forge.flashapp.me, https://mint.cashu.chat. Tokens from https://mint.coinos.io (100 ppk) and https://mint.macadamia.cash (150 ppk) must be worth the price plus the mint's NUT-02 swap fee (21 / 31 sat in practice). A short token gets 400 with `X-Cashu-Error` and is not spent. No change is returned. parameters: Url: name: url in: query required: true description: Absolute http or https URL of a public page; ports 80 and 443 only; no userinfo; fragment dropped. Percent-encode it. schema: { type: string, format: uri, maxLength: 2048 } Width: name: width in: query schema: { $ref: "#/components/schemas/Width" } Height: name: height in: query schema: { $ref: "#/components/schemas/Height" } Format: name: format in: query schema: { $ref: "#/components/schemas/Format" } Quality: name: quality in: query schema: { $ref: "#/components/schemas/Quality" } WaitMs: name: wait_ms in: query schema: { $ref: "#/components/schemas/WaitMs" } Paper: name: paper in: query schema: { $ref: "#/components/schemas/Paper" } Landscape: name: landscape in: query schema: { type: boolean, default: false } PrintBackground: name: print_background in: query schema: { type: boolean, default: true } MarginIn: name: margin_in in: query schema: { $ref: "#/components/schemas/MarginIn" } Media: name: media in: query schema: { $ref: "#/components/schemas/Media" } schemas: Width: type: integer minimum: 320 maximum: 1920 default: 1280 description: Viewport width in CSS pixels. Height: type: integer minimum: 240 maximum: 1920 default: 800 description: Viewport height in CSS pixels. Format: type: string enum: [png, jpeg, webp] default: png Quality: type: integer minimum: 1 maximum: 100 default: 80 description: Compression quality for jpeg and webp. WaitMs: type: integer minimum: 0 maximum: 10000 default: 0 description: Extra wait after the page's load event, in milliseconds. Paper: type: string enum: [letter, legal, tabloid, a3, a4, a5] default: letter MarginIn: type: number minimum: 0 maximum: 2 default: 0.4 description: Margin on all four sides, in inches. Media: type: string enum: [print, screen] default: print description: CSS media type to emulate. TargetUrl: type: string format: uri maxLength: 2048 description: Absolute http or https URL of a public page; ports 80 and 443 only; no userinfo. ScreenshotOptions: type: object properties: width: { $ref: "#/components/schemas/Width" } height: { $ref: "#/components/schemas/Height" } format: { $ref: "#/components/schemas/Format" } quality: { $ref: "#/components/schemas/Quality" } wait_ms: { $ref: "#/components/schemas/WaitMs" } PdfOptions: type: object properties: paper: { $ref: "#/components/schemas/Paper" } landscape: { type: boolean, default: false } print_background: { type: boolean, default: true } margin_in: { $ref: "#/components/schemas/MarginIn" } media: { $ref: "#/components/schemas/Media" } wait_ms: { $ref: "#/components/schemas/WaitMs" } Content: description: Exactly one of html or markdown, at most 1,048,576 bytes as UTF-8; the whole JSON body at most 2,097,152 bytes. HTML keeps its JavaScript; Markdown is sanitized into a fixed template. oneOf: - type: object required: [html] properties: html: { type: string, minLength: 1 } - type: object required: [markdown] properties: markdown: { type: string, minLength: 1 } UrlScreenshotRequest: allOf: - type: object required: [url] properties: url: { $ref: "#/components/schemas/TargetUrl" } - $ref: "#/components/schemas/ScreenshotOptions" unevaluatedProperties: false UrlPdfRequest: allOf: - type: object required: [url] properties: url: { $ref: "#/components/schemas/TargetUrl" } - $ref: "#/components/schemas/PdfOptions" unevaluatedProperties: false ContentScreenshotRequest: allOf: - $ref: "#/components/schemas/Content" - $ref: "#/components/schemas/ScreenshotOptions" unevaluatedProperties: false ContentPdfRequest: allOf: - $ref: "#/components/schemas/Content" - $ref: "#/components/schemas/PdfOptions" unevaluatedProperties: false Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: Stable machine-readable code; see https://jittator.com/docs#errors and https://jittator.com/docs#gateway-errors examples: - missing_parameter - unknown_parameter - invalid_parameter - parameter_out_of_range - invalid_request - invalid_json - invalid_url - url_too_long - scheme_not_allowed - userinfo_not_allowed - port_not_allowed - ambiguous_ip_encoding - host_not_allowed - target_not_public - robots_disallowed - not_found - method_not_allowed - content_too_large - body_too_large - unsupported_media_type - dns_resolution_failed - render_rejected - content_navigation_blocked - internal_error - target_http_error - target_unreachable - output_too_large - engine_error - engine_unavailable - busy - render_timeout - rate_limited - validation_failed - validation_unavailable - payment_backend_error message: { type: string } status: type: integer description: HTTP status; present on renderer errors, absent on gateway errors (rate_limited, validation_*, payment_backend_error, not_found from the gateway). retry_after_s: type: integer description: Present on busy, rate_limited and payment_backend_error; same value as the Retry-After header. headers: RenderMs: description: Time spent on the request, in milliseconds. schema: { type: integer } Truncated: description: Present and true when a full-page screenshot was cut at 8000 pixels. schema: { type: string, enum: ["true"] } WwwAuthenticate: description: L402 challenge, `L402 macaroon="", invoice=""`. schema: { type: string } XCashu: description: Cashu NUT-24 payment request, NUT-18 `creqA...`. schema: { type: string } RetryAfter: description: Seconds to wait before retrying. schema: { type: integer } XCashuError: description: Why a Cashu token was refused, for example how much it fell short after the mint's input fee. schema: { type: string } XL402Retry: description: After a paid L402 call failed with 422 or 5xx. `allowed` means the same Authorization header is accepted once more on the same route and method until the macaroon expires. schema: { type: string, enum: [allowed, exhausted, rate_limited, unavailable] } XCashuRefund: description: After a paid Cashu call failed with 422 or 5xx, a cashuB token from the same mint worth the route price. Treat it as money; redeem it at once and never log it. schema: { type: string } XCashuRefundError: description: Why no refund token was issued after a failed paid Cashu call. schema: { type: string, enum: [refund_failed, rate_limited, already_refunded, unavailable] } responses: Image: description: The screenshot. Not stored; save it. headers: X-Jittator-Render-Ms: { $ref: "#/components/headers/RenderMs" } X-Jittator-Truncated: { $ref: "#/components/headers/Truncated" } content: image/png: schema: { type: string, contentMediaType: image/png } image/jpeg: schema: { type: string, contentMediaType: image/jpeg } image/webp: schema: { type: string, contentMediaType: image/webp } Pdf: description: The PDF (first 50 pages at most). Not stored; save it. headers: X-Jittator-Render-Ms: { $ref: "#/components/headers/RenderMs" } content: application/pdf: schema: { type: string, contentMediaType: application/pdf } PaymentRequired: description: | The request is valid but carries no payment. Pay one offer and send the same request again. headers: WWW-Authenticate: { $ref: "#/components/headers/WwwAuthenticate" } X-Cashu: { $ref: "#/components/headers/XCashu" } content: application/json: schema: type: object required: [error] properties: error: type: object required: [code, message, price_sat] properties: code: { type: string, const: payment_required } message: { type: string } price_sat: { type: integer, description: The route's list price in sats. } BadRequest: description: | Invalid parameters (JSON error from the free check; no invoice is created), or a Cashu token that was refused (mint not listed, wrong unit, too little after the mint's fee, mint at the custody limit), in which case `X-Cashu-Error` gives the reason and the token is not spent. headers: X-Cashu-Error: { $ref: "#/components/headers/XCashuError" } content: application/json: schema: { $ref: "#/components/schemas/Error" } Unauthorized: description: | Proof not accepted: L402 credential invalid, already used, for another route or method, or expired; or a Cashu token that is malformed or already spent. Carries `WWW-Authenticate: L402`; send the request with no proof for a fresh 402. The body is not JSON. headers: WWW-Authenticate: description: The bare scheme name, `L402`. schema: { type: string } X-Cashu-Error: { $ref: "#/components/headers/XCashuError" } TooManyRequests: description: Rate limit exceeded; wait Retry-After seconds. Usually a JSON body with code rate_limited. headers: Retry-After: { $ref: "#/components/headers/RetryAfter" } content: application/json: schema: { $ref: "#/components/schemas/Error" } Error: description: | Request error, refused target, render failure or gateway error. When the call was paid and failed with 422 or 5xx, the L402 retry or Cashu refund headers are set. headers: Retry-After: { $ref: "#/components/headers/RetryAfter" } X-L402-Retry: { $ref: "#/components/headers/XL402Retry" } X-Cashu-Refund: { $ref: "#/components/headers/XCashuRefund" } X-Cashu-Refund-Error: { $ref: "#/components/headers/XCashuRefundError" } content: application/json: schema: { $ref: "#/components/schemas/Error" }