openapi: 3.2.0
info:
title: Renderwolf Render API
version: 1.0.0
summary: Screenshots, PDFs, dynamic images and video through one API.
description: Renderwolf renders web pages to images, PDFs and video.
contact:
name: Ironfang
email: hello@ironfang.uk
url: https://ironfang.uk
termsOfService: https://ironfang.uk/legal/terms
license:
name: Proprietary - use governed by the Ironfang terms of service
url: https://ironfang.uk/legal/terms
servers:
- url: https://api.ironfang.uk/renderwolf
description: Production
- url: https://api.ironfang.uk
description: Production, unpartitioned - retained for existing clients
security:
- apiKey: []
tags:
- name: Render
description: Turn a URL or HTML into an image, PDF or video.
paths:
/v1/screenshot:
post:
tags:
- Render
summary: Render a screenshot
description: Supply exactly one of `url` or `html`. Returns the image bytes.
operationId: createScreenshot
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ScreenshotRequest'
examples:
url:
summary: A page, full length
value:
url: https://example.com
width: 1280
full_page: true
element:
summary: One element, on a dark-mode page
value:
url: https://example.com/pricing
selector: .pricing-table
dark_mode: true
html:
summary: Your own markup, as JPEG
value:
html:
Hello
width: 1200
height: 630
format: jpeg
quality: 85
responses:
'200':
$ref: '#/components/responses/Image'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/RenderFailed'
'429':
$ref: '#/components/responses/RateLimited'
/v1/qr:
post:
tags:
- Render
summary: Render a QR code
description: 'Styled, optionally logo-bearing QR codes, drawn natively and
**verified before delivery**: every response is decoded and compared
to `data` on the way out. A code that would not scan is a
`422 unscannable` and the credit is returned - the guarantee is the
feature.
A logo forces the highest error-correction level and is composited
over the center on a knockout tile (`logo_pad`). Send it inline as a
`data:` URI (`logo`) or by URL (`logo_url`); remote logos face the
same target guard and rate caps as screenshot URLs.
QRs are deterministic, so identical requests hit the cache and cache
hits are free. QR costs no credits on any plan (`CostQR` is zero) -
still metered per kind, and never refused at a cap. Output never
carries the free-tier badge - a mark inside a scannable symbol would
corrupt it.
To place a QR inside a stored template render, use the `qr` object
on `POST /v1/image/{id}` instead - each entry becomes a
`{{qr.}}` variable holding a data URI.'
operationId: createQr
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/QrRequest'
examples:
plain:
summary: A link, defaults throughout
value:
data: https://example.com/menu
branded:
summary: Rounded style with a centered logo
value:
data: https://example.com
size: 600
dots: circle
dark: '#1b2a4a'
logo_url: https://example.com/logo.png
logo_size: 0.22
responses:
'200':
$ref: '#/components/responses/Image'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/RenderFailed'
'429':
$ref: '#/components/responses/RateLimited'
/v1/pdf:
post:
tags:
- Render
summary: Render a PDF
description: 'Supply exactly one of `url` or `html`. Returns `application/pdf`.
`header_html` and `footer_html` are Chromium print templates: they
support the classes `date`, `title`, `url`, `pageNumber` and
`totalPages`, which Chromium substitutes at print time.'
operationId: createPdf
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PdfRequest'
examples:
invoice:
summary: An invoice with a page footer
value:
html: Invoice 1024
print_background: true
footer_html: of
page:
summary: A page, landscape
value:
url: https://example.com/report
landscape: true
print_background: true
responses:
'200':
$ref: '#/components/responses/Pdf'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/RenderFailed'
'429':
$ref: '#/components/responses/RateLimited'
/v1/video:
post:
tags:
- Render
summary: Render a clip
description: 'A background, captions that appear on a schedule, an optional
watermark and an optional audio bed, encoded to MP4.
Rendered inside the request, so clips are capped at 60 seconds. Cost
is one credit per second of vertical or landscape output, half that
for `720p` and 0.6 for `square` - pixels are what the encoder spends.
A clip that fails to render is refunded.
Every asset URL is fetched by us through the same guard a screenshot
target faces, and each is capped at 64 MB.'
operationId: createClip
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ClipRequest'
examples:
captioned:
summary: A captioned vertical clip on a solid colour
value:
size: vertical
duration: 15
colour: '#101820'
captions:
- text: Ship it on Friday
from: 0
to: 5
- text: Find out on Monday
from: 5
to: 10
- text: Or gate the deploy
from: 10
to: 15
overBackground:
summary: Over an image, with a logo and music
value:
size: square
duration: 20
background: https://example.com/backdrop.jpg
watermark: https://example.com/logo.png
audio: https://example.com/bed.m4a
captions:
- text: New in August
from: 1
to: 6
responses:
'200':
$ref: '#/components/responses/Clip'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/RenderFailed'
'429':
$ref: '#/components/responses/RateLimited'
'501':
description: This deployment has no encoder configured.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/v1/site-preview:
post:
tags:
- Render
summary: Render a scrolling website preview
description: 'Loads and settles a website, preloads lazy content, then records a
top-to-bottom browser motion at 30 frames per second. `per_page` moves
one viewport at a time and pauses for 750ms between steps.
`single_sweep` makes one continuous eased pass. Sticky and fixed
elements behave as they do in the browser.
The response is one `multipart/form-data` payload containing the first
video frame as `poster.jpg` and the H.264 video as `preview.mp4`.
Renderwolf calculates the duration from the page height and selected
motion, up to a 60 second hard limit. Cost is one credit per output
second, rounded up. Failed work is refunded and cached repeats are free.
This endpoint is available on the product-partitioned Renderwolf URL
only. It has no unprefixed `/v1/site-preview` compatibility route.'
operationId: createSitePreview
servers:
- url: https://api.ironfang.uk/renderwolf
description: Production
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SitePreviewRequest'
examples:
showcase:
summary: A showcase card preview
value:
url: https://example.com
width: 672
height: 494
motion: per_page
responses:
'200':
$ref: '#/components/responses/SitePreview'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/RenderFailed'
'429':
$ref: '#/components/responses/RateLimited'
'501':
description: This deployment does not have browser video capture configured.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
ScreenshotRequest:
allOf:
- $ref: '#/components/schemas/RenderCommon'
- type: object
properties:
url:
type: string
format: uri
description: Page to capture. Mutually exclusive with `html`.
html:
type: string
description: Markup to render. Mutually exclusive with `url`.
width:
type: integer
default: 1280
maximum: 4096
description: Viewport width in pixels.
height:
type: integer
default: 800
maximum: 4096
description: Viewport height. Ignored when `full_page` is set.
full_page:
type: boolean
default: false
description: Capture the whole scrollable page.
selector:
type: string
description: 'CSS selector of a single element to capture instead of the
viewport.
'
clip:
$ref: '#/components/schemas/Clip'
omit_background:
type: boolean
default: false
description: 'Transparent background instead of the page''s own. Ignored for
`jpeg`, which has no alpha channel.
'
full_page_max_height:
type: integer
default: 20000
maximum: 20000
description: 'Cap a `full_page` capture at this many pixels. An
infinite-scroll page has no natural end, so 20000 applies when
none is given, and is also the most that can be asked for.
'
dark_mode:
type: boolean
default: false
description: 'Render with `prefers-color-scheme: dark`.'
format:
type: string
enum:
- png
- jpeg
- webp
default: png
description: 'PNG is lossless and the default. WebP is typically a fraction of
the size for the same picture, which is what matters when the
output is served to browsers or social crawlers.
'
quality:
type: integer
minimum: 1
maximum: 100
default: 85
description: Quality for the lossy formats, `jpeg` and `webp`. Ignored for PNG.
device:
type: string
enum:
- desktop
- tablet
- mobile
description: 'Capture as a phone or tablet rather than a desktop window that
happens to be narrow. Sets the viewport, the device pixel ratio,
touch, and a matching mobile user agent together - width alone
gets media queries right and everything else wrong.
An explicit `width` or `height` still wins, so a preset can be
used for the density and user agent while overriding the size.
'
delay_ms:
type: integer
minimum: 0
maximum: 10000
description: 'Extra settle time after load. Renderwolf watches the page and
captures when it stops changing, so this is rarely needed - use
it for content that arrives on a timer, which quiescence cannot
anticipate. Capped at ten seconds.
'
RenderCommon:
type: object
properties:
no_cache:
type: boolean
default: false
description: 'Force a fresh render instead of serving a cached one. Always
billable. Excluded from the cache key, so repeated fresh requests
do not collide.
'
block_ads:
type: boolean
default: false
description: 'Drop requests to known ad and tracker networks before they load.
Stopped as network requests rather than hidden afterwards, so the
page never spends time fetching them.
'
block_cookie_banners:
type: boolean
default: false
description: 'Hide common consent banners, and undo the scroll lock they set -
without which a full-page capture is one viewport tall.
Hidden, not accepted. Clicking "accept" would be a decision made
on the site owner''s behalf and recorded as theirs.
'
hide_selectors:
type: array
items:
type: string
description: 'CSS selectors to hide before capturing. Applied after load, so
elements injected by script are covered.
'
headers:
type: object
additionalProperties:
type: string
description: Extra HTTP headers sent with every request for the page.
cookies:
type: array
items:
$ref: '#/components/schemas/Cookie'
description: 'Cookies to set before navigating. Set beforehand because a session
cookie that arrives after load has missed the request it was meant
to authenticate.
'
authorization:
type: string
description: 'Sets the `Authorization` header - the common case, spelled once.
An explicit `Authorization` in `headers` wins over this.
'
user_agent:
type: string
description: 'Overrides the user agent, including any set by `device`.
'
wait_until:
type: string
enum:
- load
- domcontentloaded
- networkidle
default: load
description: 'When the page counts as ready. `networkidle` waits for traffic to
stop and is bounded by the render timeout, because a page with
polling never truly idles.
'
wait_for_selector:
type: string
description: 'Wait for this element before capturing. Fails the render if it
never appears, rather than returning a half-drawn page.
'
timeout_ms:
type: integer
description: 'Per-render timeout. Clamped to the service maximum - it can lower
the ceiling but not raise it.
'
device_scale_factor:
type: number
minimum: 1
maximum: 3
description: 'Pixel density. 2 is retina: the same CSS size at twice the pixels.
'
Margin:
type: object
description: 'Page margins in inches. Omitted sides keep Chromium''s default rather
than becoming zero - a PDF printed hard to the paper edge is rarely
what leaving a field out was meant to mean.
'
properties:
top:
type: number
right:
type: number
bottom:
type: number
left:
type: number
ClipRequest:
type: object
description: 'Everything is optional. With no background a solid `colour` is used,
which is what most captioned clips want.
'
properties:
size:
type: string
enum:
- vertical
- square
- landscape
- 720p
default: vertical
description: 'vertical 1080x1920, square 1080x1080, landscape 1920x1080,
720p 1280x720.
'
duration:
type: number
default: 15
minimum: 1
maximum: 60
description: Seconds of output.
colour:
type: string
pattern: ^#[0-9a-fA-F]{6}$
default: '#101820'
description: Background colour, used when no background asset is given.
background:
type: string
format: uri
description: An image or video to fill the canvas, cropped to fit.
watermark:
type: string
format: uri
description: A PNG placed in the bottom right corner.
audio:
type: string
format: uri
description: An audio track, trimmed to the clip's length.
font_size:
type: integer
minimum: 12
maximum: 200
description: Caption size in pixels; defaults to a fifteenth of the width.
captions:
type: array
maxItems: 12
items:
type: object
required:
- text
- to
properties:
text:
type: string
maxLength: 280
from:
type: number
description: Seconds from the start.
to:
type: number
PdfRequest:
allOf:
- $ref: '#/components/schemas/RenderCommon'
- type: object
properties:
url:
type: string
format: uri
description: Page to print. Mutually exclusive with `html`.
html:
type: string
description: Markup to print. Mutually exclusive with `url`.
landscape:
type: boolean
default: false
paper_format:
type: string
enum:
- a3
- a4
- a5
- letter
- legal
- tabloid
description: 'Named page size. Omit for Chromium''s default, which is Letter.
Sizes are portrait; `landscape` rotates them.
'
margin:
$ref: '#/components/schemas/Margin'
print_background:
type: boolean
default: false
description: 'Include background colours and images. Off by default, matching
a browser''s print dialog.
'
header_html:
type: string
description: Chromium print header template.
footer_html:
type: string
description: Chromium print footer template.
scale:
type: number
description: Print scale. Omit or `0` for 1.0.
Cookie:
type: object
required:
- name
- domain
properties:
name:
type: string
value:
type: string
domain:
type: string
description: 'Required. Chromium silently drops a domainless cookie set before
navigation, so the render would quietly come back logged out.
'
path:
type: string
default: /
Error:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
properties:
code:
type: string
description: Stable, safe to branch on.
enum:
- bad_request
- unauthorized
- invalid_api_key
- auth_failed
- forbidden
- email_unverified
- not_found
- quota_exhausted
- rate_limited
- target_rate_limited
- render_failed
- bad_signature
- signing_disabled
message:
type: string
description: Human-readable. May change; do not match on it.
example:
error:
code: target_rate_limited
message: too many renders for that host, try again shortly
Clip:
type: object
description: A rectangle in CSS pixels from the top left of the page.
required:
- width
- height
properties:
x:
type: number
default: 0
y:
type: number
default: 0
width:
type: number
maximum: 4096
height:
type: number
maximum: 4096
SitePreviewRequest:
allOf:
- $ref: '#/components/schemas/RenderCommon'
- type: object
required:
- url
properties:
url:
type: string
format: uri
description: Public HTTP or HTTPS page to record.
width:
type: integer
default: 672
minimum: 320
maximum: 1280
multipleOf: 2
description: Output and browser viewport width in pixels.
height:
type: integer
default: 494
minimum: 240
maximum: 1200
multipleOf: 2
description: 'Output and browser viewport height in pixels. Width multiplied
by height cannot exceed 1,200,000 pixels.
'
motion:
type: string
enum:
- per_page
- single_sweep
default: per_page
description: '`per_page` moves one viewport at a time with eased transitions
and a 750ms reading pause. `single_sweep` makes one
continuous eased pass from top to bottom.
'
dark_mode:
type: boolean
default: false
description: 'Record with `prefers-color-scheme: dark`.'
device:
type: string
enum:
- desktop
- tablet
- mobile
description: 'Use a full device preset. Explicit width and height still win.
'
delay_ms:
type: integer
minimum: 0
maximum: 10000
description: Extra settle time after page load and before lazy-content preloading. Capped at ten seconds.
no_cache:
type: boolean
default: false
description: Force a fresh recording instead of using a cached result.
QrRequest:
type: object
required:
- data
properties:
data:
type: string
maxLength: 1024
description: The payload - a URL, WiFi string, or any text up to 1KB.
size:
type: integer
default: 512
minimum: 64
maximum: 2048
description: Output side in pixels. Output is always square PNG.
ecc:
type: string
enum:
- L
- M
- Q
- H
default: M
description: 'Error-correction level. Forced to `H` when a logo is present -
the logo destroys the modules it covers, and that budget has to
come from somewhere.
'
dark:
type: string
default: '#000000'
description: Module color, `#rgb` or `#rrggbb`. Must actually be darker than `light`.
light:
type: string
default: '#ffffff'
description: 'Background color, or `transparent`. Transparent codes are
verified against a white ground - place them on light surfaces.
'
dots:
type: string
enum:
- square
- circle
default: square
description: 'Module style. `circle` draws separated dots. Finder patterns
always render solid - styled finders are how QR generators
produce codes that do not scan.
'
eyes:
type: string
enum:
- square
- rounded
description: Finder-pattern style. Defaults to match `dots`.
margin:
type: integer
default: 4
minimum: 0
maximum: 8
description: Quiet zone, in modules.
invert:
type: boolean
default: false
description: 'Light modules on a dark ground. Explicit because it narrows the
audience: modern phone cameras read inverted codes, many
embedded and in-app scanners do not - use it for screens, not
print or payments. With the flag set, `dark` (the module color)
must be lighter than `light` (the ground). Verified against the
negative, so the structural guarantee holds.
'
logo:
type: string
description: 'Center mark as a base64 `data:` URI, png or jpeg, up to 1MB
decoded. Mutually exclusive with `logo_url`.
'
logo_url:
type: string
format: uri
description: Center mark by URL. SSRF-guarded and rate-capped like a render target.
logo_size:
type: number
default: 0.22
minimum: 0.12
maximum: 0.3
description: Logo tile as a fraction of the symbol width.
logo_pad:
type: boolean
default: true
description: Knockout tile behind the logo so it sits on clean ground.
responses:
RateLimited:
description: "Three distinct conditions share this status, separated by `error.code`:\n\n- `rate_limited` - your account exceeded 120 renders/minute\n- `target_rate_limited` - the site being rendered is being hit too hard\n across all customers (60/minute per host)\n- `quota_exhausted` - the plan's monthly allowance is spent\n\nThe first two clear within the minute, so retry with backoff. The third\ndoes not: upgrade in the portal or wait for the period to roll over.\nBranch on the code rather than the status.\n"
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Pdf:
description: The rendered PDF.
headers:
X-Renderwolf-Cache:
schema:
type: string
enum:
- hit
X-Renderwolf-Credits:
description: 'What this call cost. `0` on a cache hit, because a cache hit is
free. Read it to log spend per request without polling
`/v1/usage`.
'
schema:
type: integer
X-Renderwolf-Render-Ms:
schema:
type: integer
content:
application/pdf:
schema:
type: string
format: binary
SitePreview:
description: The poster image and scrolling MP4 returned as two file parts.
headers:
X-Renderwolf-Cache:
schema:
type: string
enum:
- hit
X-Renderwolf-Render-Ms:
schema:
type: integer
X-Renderwolf-Credits:
description: 'What this preview cost. `0` on a cache hit, because a cache hit is
free. The poster is included in the video cost.
'
schema:
type: integer
X-Renderwolf-Output-Seconds:
description: Calculated video length in seconds. Present on fresh renders.
schema:
type: number
content:
multipart/form-data:
schema:
type: object
required:
- poster
- video
properties:
poster:
type: string
format: binary
description: JPEG first frame, suitable for a video poster.
video:
type: string
format: binary
description: H.264 MP4 with fast-start metadata.
Clip:
description: The rendered clip.
headers:
X-Renderwolf-Cache:
schema:
type: string
enum:
- hit
X-Renderwolf-Render-Ms:
schema:
type: integer
X-Renderwolf-Credits:
description: 'What this clip cost. `0` on a cache hit, because a cache hit is
free.
'
schema:
type: integer
content:
video/mp4:
schema:
type: string
format: binary
BadRequest:
description: Malformed body, or neither/both of `url` and `html`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Image:
description: 'The rendered image. `png` unless `format: jpeg` was requested.
'
headers:
X-Renderwolf-Cache:
description: '`hit` when served from cache, in which case it was free.'
schema:
type: string
enum:
- hit
X-Renderwolf-Credits:
description: 'What this call cost. `0` on a cache hit, because a cache hit is
free. Read it to log spend per request without polling
`/v1/usage`.
'
schema:
type: integer
X-Renderwolf-Render-Ms:
description: Render time in milliseconds, excluding any requested delay.
schema:
type: integer
X-Renderwolf-Delay-Ms:
description: The `delay_ms` actually waited, when one was requested.
schema:
type: integer
X-Renderwolf-Captured-At:
description: 'RFC 3339 timestamp of a fresh capture. Present only on
`no_cache` renders, where the response is also `no-store` - the
pairing is what makes a capture usable as evidence.
'
schema:
type: string
format: date-time
content:
image/png:
schema:
type: string
format: binary
image/jpeg:
schema:
type: string
format: binary
Unauthorized:
description: Missing, malformed, revoked or unknown API key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RenderFailed:
description: 'The page did not render: the host did not resolve, it never finished
loading, it timed out, or it was blocked. Try `delay_ms`, and check the
target loads in a normal browser.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
apiKey:
type: http
scheme: bearer
description: 'An API key from the portal, sent as `Authorization: Bearer rw_live_...`.
'