Ironfang Render
API reference
Find endpoints, request parameters, authentication, quotas, response headers and error codes for the Ironfang Render API.
Quickstart
All endpoints use https://api.ironfang.uk/renderwolf as the base URL. The product name appears in the path because api.ironfang.uk hosts all Ironfang APIs. The legacy unprefixed https://api.ironfang.uk/v1/ path remains supported for endpoints that previously shipped there. New endpoints use the product base URL. Create an API key in the portal, then render your first screenshot:
curl -X POST https://api.ironfang.uk/renderwolf/v1/screenshot \
-H "Authorization: Bearer if_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "width": 1280}' \
--output shot.pngSuccessful render requests return the binary asset directly as image/png, image/jpeg or application/pdf. Clips return video/mp4. Site previews return a multipart response containing a JPEG poster and MP4 video. Other successful endpoints and all error responses return JSON.
Client libraries
Official clients wrap the same API - typed requests, the webhook verifier, and result downloads included:
npm install @ironfang/renderwolf # TypeScript, Node 20+
pip install ironfang-renderwolf # PythonThere is also a standalone CLI - single binaries for Linux, macOS and Windows with checksums, from github.com/ironfang-ltd/renderwolf-cli.
Postman
Every endpoint is in a public Postman collection generated from the OpenAPI contract, with a Getting started folder whose first request needs no key.
Authentication
Send your API key as a bearer token with every request: Authorization: Bearer if_live_.... Create and revoke keys in the portal. The secret is displayed once when the key is created and is stored as a hash. A missing or unrecognised key returns 401 invalid_api_key.
Scopes
An Ironfang API key can provide access to multiple products. Select its scopes when you create it. Scopes cannot be expanded later, so create a new key when broader access is required. A request outside the key's scopes returns 403 insufficient_scope and identifies the missing scope.
renderwolf:renderScreenshots, PDFs, images, clips and site previewsrenderwolf:signCreate signed render URLsrenderwolf:templates:readRead stored templatesrenderwolf:templates:writeCreate, edit and delete templatesrenderwolf:usage:readRead usage against the quota
Keys created before scopes were introduced have no assigned scopes and retain full access for backward compatibility. New keys are always created with explicit scopes.
POST /v1/screenshot
Capture a URL or raw HTML with a warm Chromium instance. Uses 1 credit.
| Field | Type | Notes |
|---|---|---|
url | string | Page to capture. Provide url or html, not both. Private-network targets are blocked. |
html | string | Raw HTML to render instead of a URL. |
width | int | Viewport width. Default 1280. |
height | int | Viewport height. Default 800. |
device | string | desktop, tablet or mobile. Each preset applies a viewport, pixel density and user agent. See Devices. |
full_page | bool | Capture the full scroll height instead of the viewport. |
selector | string | CSS selector for capturing a single element. |
dark_mode | bool | Emulate prefers-color-scheme: dark. |
format | string | png (default), jpeg or webp. WebP can produce a smaller file than PNG for the same capture. |
quality | int | JPEG and WebP quality from 1 to 100. Default 85. |
delay_ms | int | Additional wait time after load for late-rendering content. |
no_cache | bool | Force a new capture, ignoring any cached render. Counts against your quota. See Caching. |
clip | object | { x, y, width, height } in CSS pixels from the top left. This field is ignored when selector is set because the selector defines the capture region. |
omit_background | bool | Use a transparent background instead of the page background. This field is ignored for jpeg, which has no alpha channel. |
full_page_max_height | int | Caps a full_page capture. An infinite-scroll page has no natural end without it. |
Before capture, Ironfang Render waits for web fonts, advances CSS animations and transitions to their final state, and hides scrollbars. Looping animations continue running.
Sites with bot protection may serve a verification page to automated browsers. Ironfang Render returns the content supplied by the site and does not attempt to bypass the verification step.
Use delay_ms when content continues rendering after the standard page-settling process. Common examples include dashboards that fetch data after load and pages animated by JavaScript libraries such as GSAP.
If elements are missing or half-faded in your capture, try delay_ms between 500 and 1500.
curl -X POST https://api.ironfang.uk/renderwolf/v1/screenshot \
-H "Authorization: Bearer if_live_..." \
-d '{"url": "https://example.com", "full_page": true, "dark_mode": true}' \
--output page.pngDevices
The device field applies a viewport, pixel density, mobile identity and user agent together. This provides more accurate device emulation than changing the viewport width alone.
| Device | Viewport | Pixel ratio | Image size |
|---|---|---|---|
desktop | 1280 × 800 | 1× | 1280 × 800 |
tablet | 820 × 1180 | 2× | 1640 × 2360 |
mobile | 390 × 844 | 3× | 1170 × 2532 |
Viewport dimensions use CSS pixels. An explicit width or height overrides the corresponding preset value. An unrecognised device name returns a validation error.
Device presets use Chromium device emulation. They do not bypass bot protection, so a site may still return a verification page.
curl -X POST https://api.ironfang.uk/renderwolf/v1/screenshot \
-H "Authorization: Bearer if_live_..." \
-d '{"url": "https://example.com", "device": "mobile", "format": "webp"}' \
--output phone.webpOptions for every render
These options apply to screenshots, PDFs and site previews. Omitting an option preserves the default response behaviour.
Blocking
Ads and trackers are blocked as network requests before they load. Consent banners are page elements inserted by scripts, so Ironfang Render hides supported banners with CSS after page load.
| Field | Type | Notes |
|---|---|---|
block_ads | bool | Prevents requests to known ad and tracker networks. Ironfang Render uses a focused blocklist to reduce the risk of removing resources needed by the page. |
block_cookie_banners | bool | Hides supported consent frameworks and removes their page scroll locks. This allows full_page captures to extend beyond one viewport. Ironfang Render does not click an accept button or record consent. |
hide_selectors | string[] | CSS selectors to hide. Applied after load, so elements injected by script are covered. |
Custom CSS, JavaScript and page actions
Hide a widget, adjust print styles, select a tab or expand content before a screenshot or PDF. These options work with a URL or supplied HTML, including queued jobs and batches.
After the normal page load and waiting conditions, Ironfang Render applies built-in hiding, then your css, awaits your script, and runs actions in order. Settling and capture happen afterwards. Top-level wait_for_selector runs before these controls; use a selector action to wait for content produced by an interaction.
| Field | Type | Limits and behavior |
|---|---|---|
css | string | A stylesheet up to 65,536 UTF-8 bytes. Supports layout overrides, hidden elements, disabled animations and @media print. Malformed syntax returns 400 bad_request before rendering. Chromium determines supported property names and values. |
script | string | JavaScript up to 16,384 UTF-8 bytes, executed inside the target page's main-frame context as an async function body. Use await or return a promise for asynchronous work. Execution is limited to five seconds or the remaining render timeout, whichever is shorter. Return values are discarded. |
actions | object[] | Up to 20 ordered actions sharing the remaining timeout_ms budget. Selectors target the first match in the main page and are limited to 1,024 UTF-8 bytes. |
| Action type | Required field | Behavior |
|---|---|---|
click | selector | Wait for an interactable, enabled element and click it. |
hover | selector | Wait for an interactable element and move the pointer over it. |
wait_for_selector | selector | Wait for an element to exist; it may be hidden. |
delay | duration_ms | Wait 0-10,000 ms. All action delays together may total at most 10,000 ms. |
This self-contained example hides a widget, changes the heading and expands pricing details before capture. Replace html with urland use your site's selectors to capture an existing page.
curl https://api.ironfang.uk/renderwolf/v1/screenshot \
-H "Authorization: Bearer $IRONFANG_API_KEY" \
-H 'Content-Type: application/json' \
--output expanded.png --data-binary @- <<'JSON'
{
"html": "<h1 id='title'>Plans</h1><div class='chat-widget'>Chat widget</div><details id='details'><summary id='show-more'>Show pricing</summary><p>Expanded pricing details</p></details>",
"css": ".chat-widget { display: none !important; }",
"script": "document.querySelector('#title').textContent = 'Pricing';",
"actions": [
{ "type": "click", "selector": "#show-more" },
{ "type": "wait_for_selector", "selector": "#details[open]" }
]
}
JSONUse /v1/pdf and --output expanded.pdf for the same page state in a PDF. For asynchronous JavaScript, use an awaited operation, for example await new Promise(resolve => setTimeout(resolve, 250));. For UI that arrives later, follow the interaction with a selector action.
Invalid inputs return 400 bad_request. Script syntax errors, execution failures and timeouts return 422 render_failed with a sanitized message. Action errors identify the zero-based index and type, such as actions[0] (click), and stop the list. Failed renders are refunded; queued jobs do not retry these interaction failures.
These options and action order are part of the screenshot cache key. A cache hit returns the previous image without executing controls again; set no_cache: true for a fresh run. Scripts and clicks can change the target application's state. The API exposes page-context snippets and these four actions; it does not provide browser sessions or Playwright/Puppeteer APIs.
Headers, cookies and credentials
Ironfang Render applies headers, cookies and credentials before navigation so they are present on the first request to the target page.
| Field | Type | Notes |
|---|---|---|
headers | object | Extra HTTP headers, sent with every request for the page. |
authorization | string | Sets the Authorization header. An explicit Authorization entry in headers takes precedence. |
cookies | object[] | { name, value, domain, path }. domain is required because Chromium drops a domainless cookie set before navigation. |
user_agent | string | Overrides the user agent, including one set by device. |
Private-network targets remain blocked when credentials are supplied. The SSRF guard runs before authentication data is applied.
Waiting
| Field | Type | Notes |
|---|---|---|
wait_until | string | load (default), domcontentloaded or networkidle. networkidle waits for traffic to stop and is bounded by the render timeout, because a page that polls never truly idles. |
wait_for_selector | string | Wait for this element before capturing. The request fails if the element does not appear before the timeout. |
timeout_ms | int | Per-render timeout. Values above the service maximum are clamped to that maximum. |
device_scale_factor | number | Pixel density, 1 to 3. 2 is retina: the same CSS size at twice the pixels. |
curl -X POST https://api.ironfang.uk/renderwolf/v1/screenshot \
-H "Authorization: Bearer if_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"block_ads": true,
"block_cookie_banners": true,
"wait_for_selector": "#main",
"device_scale_factor": 2
}' \
--output shot.pngPOST /v1/pdf
Print a URL or raw HTML to PDF. Each successful PDF uses 2 credits.
| Field | Type | Notes |
|---|---|---|
url | string | Page to print. Provide url or html. |
html | string | Raw HTML to print instead of a URL. |
landscape | bool | Landscape orientation. |
print_background | bool | Include CSS backgrounds. |
header_html | string | Chromium header template. |
footer_html | string | Chromium footer template. |
scale | float | Print scale. Default 1.0. |
no_cache | bool | Force a new capture, ignoring any cached render. Counts against your quota. See Caching. |
paper_format | string | a3, a4, a5, letter, legal or tabloid. Omit for Chromium's default, Letter. Sizes are portrait; landscape rotates them. |
margin | object | { top, right, bottom, left } in inches. Omitted sides retain Chromium's default margin. |
curl -X POST https://api.ironfang.uk/renderwolf/v1/pdf \
-H "Authorization: Bearer if_live_..." \
-d '{"html": "<h1>Invoice #42</h1>", "print_background": true}' \
--output invoice.pdfPOST /v1/qr
Generate styled QR codes that are verified before delivery. Ironfang Render decodes each finished image and compares it with the supplied payload. Failed verification returns 422 unscannable. QR requests are free on every plan, including inside templates. They appear in usage records but consume zero credits and remain available at the plan cap.
| Field | Type | Notes |
|---|---|---|
data | string | Required payload containing a URL, WiFi string or text up to 1KB. |
size | int | Square output size from 64 to 2048 pixels. Default 512. PNG only. |
ecc | string | Error correction: L, M, Q or H. Default M. Adding a logo automatically selects H to compensate for the modules covered by the logo. |
dark / light | string | Hex colours. dark must be darker than light and meet the minimum contrast requirement. Invalid combinations return a 400 response. light also accepts transparent. Place transparent codes on a light background. |
dots | string | square or circle. Finder patterns always render solid to preserve their required structure. |
eyes | string | square or rounded. Defaults to match dots. |
margin | int | Quiet zone from 0 to 8 modules. Default 4. |
invert | bool | Creates light modules on a dark background. Modern phone cameras often read inverted codes, but many embedded and in-app scanners do not. With this option, dark must be lighter than light. Ironfang Render verifies the result against its negative. |
logo | string | Center mark as a base64 data: URI, png or jpeg. Mutually exclusive with logo_url. |
logo_url | string | Centre mark supplied by URL. The URL uses the same target guard and rate limits as a screenshot request. |
logo_size | float | Logo tile as a fraction of the symbol, from 0.12 to 0.30. Default 0.22. |
logo_pad | bool | Adds a knockout tile behind the logo to separate it from the QR modules. Default true. |
curl -X POST https://api.ironfang.uk/renderwolf/v1/qr \
-H "Authorization: Bearer if_live_..." \
-d '{"data": "https://example.com/menu", "dots": "rounded",
"logo_url": "https://example.com/logo.png"}' \
--output qr.pngTo include a QR code in a stored template, use the qr object on template renders. QR output does not include the free-plan badge because an added mark could affect scanning.
POST /v1/video
A clip: a background, captions that appear on a schedule, an optional watermark and an optional audio bed. Returns video/mp4.
Ironfang Render generates short clips from structured data for automated content workflows. Requests use the same API key, quota and integrations as other render types.
| Field | Type | Notes |
|---|---|---|
size | string | vertical 1080×1920 (default), square 1080×1080, landscape 1920×1080, 720p 1280×720. |
duration | number | Seconds of output, 1 to 60. Default 15. |
captions | array | Up to 12 cards of {text, from, to}, in seconds. Text may include quotes, colons and brackets. |
colour | string | Hex background, used when no background is given. Default #101820. |
background | string | URL of an image or video, cropped to fill the canvas. |
watermark | string | URL of a PNG, placed bottom right. |
audio | string | URL of an audio track, trimmed to the clip. |
font_size | int | Caption size in pixels. Defaults to a fifteenth of the width. |
curl -X POST https://api.ironfang.uk/renderwolf/v1/video \
-H "Authorization: Bearer if_live_..." \
-d '{
"size": "vertical",
"duration": 15,
"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}
]
}' --output clip.mp4Credit usage
Vertical and landscape clips use one credit per second. Square clips use 0.6 credits per second, and 720p clips use 0.5 credits per second. The total is rounded to the nearest whole credit with a minimum of one. A 15-second vertical clip uses 15 credits, while the same clip at 720p uses 8. Failed work is refunded, and identical cached requests use zero credits.
Operational limits
Clips are rendered inside the request and capped at 60 seconds. Referenced assets use the same URL checks as screenshot targets and are capped at 64 MB each. Background assets must be available from the public internet.
Clips are limited to 60 seconds and rendered synchronously. Workflows that require longer clips, queued jobs or callbacks are not currently supported.
POST /renderwolf/v1/site-preview
Turn a public webpage into a top-to-bottom preview. Choose a continuous sweep or flick-eased, viewport-sized steps that settle into a short reading pause. One request returns a JPEG poster from the first frame and a browser-recorded H.264 MP4. Fixed and sticky elements behave as they do during a real scroll.
This endpoint is available only at https://api.ironfang.uk/renderwolf/v1/site-preview.
| Field | Type | Notes |
|---|---|---|
url | string | Public HTTP or HTTPS page to record. Required. |
width | int | Even output width from 320 to 1280. Default 672. |
height | int | Even output height from 240 to 1200. Default 494. The frame is limited to 1,200,000 pixels. |
motion | string | per_page for viewport-sized steps and pauses, or single_sweep for one continuous pass. Default per_page. |
device | string | desktop, tablet or mobile. |
dark_mode | bool | Record with the page's dark colour scheme. Default false. |
no_cache | bool | Force a fresh recording. Default false. |
Run this from your server, never from browser code: the request carries your API key, and anything shipped to a browser is public. For a preview a visitor triggers, mint a signed URL server-side instead.
const response = await fetch(
'https://api.ironfang.uk/renderwolf/v1/site-preview',
{
method: 'POST',
headers: {
Authorization: 'Bearer if_live_...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://example.com',
motion: 'per_page'
})
}
);
const output = await response.formData();
const poster = output.get('poster'); // poster.jpg, image/jpeg
const video = output.get('video'); // preview.mp4, video/mp4Output and credits
The response is multipart/form-data with named poster and video parts. Video is 60fps H.264 MP4 with a first-frame JPEG poster. Ironfang Render calculates the duration from the page height and motion, with a 60-second server limit. Site previews use one credit per output second, rounded up. The poster is included. Failed work is refunded, and identical cached requests use zero credits.
Templates
Store reusable HTML and render it with different variables for Open Graph images, social cards and other dynamic assets. Variables use {{name}} placeholders. Substituted values are HTML-escaped to prevent markup injection. Template management is free, while each successful image render uses one credit. IDs use UUIDv7 and sort by creation time.
| Endpoint | Purpose |
|---|---|
POST/v1/templates | Create. Body: name, html (max 256KB), width, height (default 1200×630, capped 4096). |
GET/v1/templates | List your templates. |
GET/v1/templates/{id} | Fetch one. |
PUT/v1/templates/{id} | Update. Edits invalidate the render cache automatically. |
DELETE/v1/templates/{id} | Delete. |
curl -X POST https://api.ironfang.uk/renderwolf/v1/templates \
-H "Authorization: Bearer if_live_..." \
-d '{
"name": "og-card",
"html": "<div class=\"card\"><h1>{{title}}</h1><p>{{author}}</p></div>",
"width": 1200,
"height": 630
}'POST /v1/image/{id}
Render a stored template as an image with the supplied variables. Each successful image uses 1 credit. Identical cached requests use zero credits.
| Field | Type | Notes |
|---|---|---|
vars | object | String map filling the template's {{placeholders}}. Missing vars render empty. |
no_cache | bool | Force a new capture, ignoring any cached render. Counts against your quota. See Caching. |
format | string | png (default), jpeg or webp. WebP can produce a smaller file than PNG for the same output. |
qr | object | Named QR specs, up to 4, each the shape of the /v1/qr body. Each entry is rendered, scan-verified and injected as a {{qr.<name>}} variable containing a data URI for use in an <img> element. Embedded QR codes add no charge to the image render. |
curl -X POST https://api.ironfang.uk/renderwolf/v1/image/0198c9f1-5b7a-7c2e-9f1d-3a8b2c4d5e6f \
-H "Authorization: Bearer if_live_..." \
-d '{"vars": {"title": "Hello from Ironfang Render", "author": "Rick"}}' \
--output og.pngFor a QR slot, include <img src="{{qr.pay}}" width="150"> in the template:
curl -X POST https://api.ironfang.uk/renderwolf/v1/image/0198c9f1-5b7a-7c2e-9f1d-3a8b2c4d5e6f \
-H "Authorization: Bearer if_live_..." \
-d '{"vars": {"invoice": "INV-2041"},
"qr": {"pay": {"data": "https://pay.example/INV-2041", "size": 300}}}' \
--output invoice-card.pngSigned URLs
Create a stable GET URL for use in an <img> element or og:image meta tag. The URL does not expose the API key or require an application proxy. Its signature covers every parameter and the account used for metering, preventing changes after signing.
POST /v1/sign with:
| Field | Type | Notes |
|---|---|---|
kind | string | screenshot or image. |
url | string | Target page (kind: screenshot). |
template | string | Template id (kind: image). |
vars | object | Template variables (kind: image). |
width / height | int | Optional dimensions. |
full_page | bool | Screenshots only. |
ttl_hours | int | Expiry. 0 = never expires. |
The response contains the full url (and its path). Fetching it renders on demand and meters against your account. Repeated fetches of an identical cached result use zero credits.
curl -X POST https://api.ironfang.uk/renderwolf/v1/sign \
-H "Authorization: Bearer if_live_..." \
-d '{"kind": "image", "template": "0198c9f1-5b7a-7c2e-9f1d-3a8b2c4d5e6f", "vars": {"title": "My post"}, "ttl_hours": 0}'
{"url": "https://api.ironfang.uk/v1/r/0/a1b2c3...?kind=image&template=0198c9f1-5b7a-7c2e-9f1d-3a8b2c4d5e6f&v.title=My+post"}Jobs
Every render above can also run as a durable job: submit it, get a 202 back at once, poll until it finishes, then collect the result. Use jobs for clips, slow pages, and anywhere you would rather not hold a connection open. The synchronous endpoints are unchanged.
POST /renderwolf/v1/jobs
Idempotency-Key: catalogue-42-v1
{
"kind": "screenshot",
"request": { "url": "https://example.com", "full_page": true },
"external_id": "catalogue-42"
}kind is screenshot, pdf, qr, image, clip or site_preview, and request is exactly that endpoint's body (an image job names its template inside request). The submission is validated as the synchronous endpoint would validate it, before anything is charged.
Polling and results
GET /renderwolf/v1/jobs/{id} returns the job with status of queued, running, cancellation_requested, succeeded, failed or cancelled. A succeeded job carries result - content type, size, SHA-256, expiry - and a signed url that needs no API key and is valid for 15 minutes; GET .../result redirects to a fresh one. The hosted result is kept for 24 hours after success, privately, and then removed: this is a collection window, not asset hosting. Store what you need on your side.
Idempotency, cancellation and credits
Send Idempotency-Key on every submission. The same key with the same request returns the existing job; the same key with a different request is a 409 idempotency_conflict. DELETE .../jobs/{id} cancels: a queued job stops at once, a running one at its next safe point.
The job's maximum cost is reserved when it is accepted and settled when it finishes - a site preview reserves the 60-second ceiling and releases the difference once the page has been measured. Failed work is refunded. A queued job cancels for free; a running one is charged only if it produced a usable output. Transient failures are retried up to three times with backoff; a page that refuses to render is failed at once with the reason.
Batches
Up to 100 jobs in one submission: a shared default request plus items that override parts of it. Useful when what you actually have is a list of URLs and one set of options.
POST /renderwolf/v1/batches
Idempotency-Key: nightly-2026-08-28
{
"kind": "screenshot",
"default": { "full_page": true, "block_ads": true },
"external_id": "nightly",
"items": [
{ "request": { "url": "https://example.com/a" }, "external_id": "a" },
{ "request": { "url": "https://example.com/b" }, "external_id": "b" },
{ "kind": "pdf", "request": { "url": "https://example.com/terms" } }
]
}The merge is one level deep: an item's field wins, everything else comes from the default. An item may also override kind and carry its own delivery.
A batch is accepted or refused whole. Every item is validated before any is stored, and the credits for all of them are reserved in one transaction - so a batch that would exceed your monthly credits comes back as 429 quota_exhausted having charged nothing and left no jobs behind. A validation error names the item it came from.
Items are ordinary jobs: poll each one, or poll GET /renderwolf/v1/batches/{id} for counts by state, done, and the full job resource for every item. There is no ZIP of results - configure a storage destination if you want the output collected in one place, which is also the only version of this that works when the batch is 100 clips.
Delivery
A job can do more than sit and wait to be collected. Register a destination - a webhook endpoint, or your own S3-compatible bucket - and name it by id from any job. Credentials are sent to us once, when the destination is created, and never travel in a job body.
POST /renderwolf/v1/destinations
{ "type": "webhook", "name": "Production", "url": "https://hooks.example.com/render" }
→ 201 { "id": "0198f0a1...", "signing_secret": "...", "signing_secret_note": "store this now; it is not shown again" }The signing secret is shown exactly once. We keep only an encrypted copy, so there is no endpoint that can return it to you later; if you lose it, create a new destination. Managing destinations needs the renderwolf:destinations scope.
POST /renderwolf/v1/jobs
{
"kind": "screenshot",
"request": { "url": "https://example.com" },
"external_id": "catalogue-42",
"delivery": {
"webhook_destination": "0198f0a1...",
"storage_destination": "0198f0b2...",
"storage_key": "captures/{date}/{external_id}.png"
}
}Signed webhooks
A webhook destination is called on render.job.succeeded, render.job.failed and render.job.cancelled. The body carries the job, its credits, and - on success - the result's content type, size, SHA-256 and a signed url good for 15 minutes from the moment we sent it, not from when the job finished.
Verify the signature over the raw bytes you received, not over anything you parsed and re-serialised:
expected = "v1=" + hmac_sha256(secret, timestamp + "." + raw_body).hexdigest()
compare(expected, headers["Renderwolf-Signature"]) # constant timeRenderwolf-Timestamp is Unix seconds; reject anything more than a few minutes old to stop a replay. Renderwolf-Event-Id is stable across retries of the same delivery, so use it to make your handler idempotent.
Retries follow a ladder from one minute out to a day, with jitter, honouring a bounded Retry-After. We retry 408, 429 and 5xx; other 4xx stop immediately, because they are your endpoint saying a retry will not help. After 10 consecutive failures the destination is disabled until you enable it again in the portal.
Delivery to your own bucket
An s3 destination takes a bucket, a region, credentials and an optional endpoint and prefix. Anything S3-compatible works: AWS, Cloudflare R2, MinIO, Ceph. Scope the credentials to the prefix you give us - they only ever need to write there.
storage_key is the object key relative to that prefix. It takes fixed text plus {job_id}, {external_id} and {date} (YYYY/MM/DD) - no other field, and nothing evaluated. It defaults to renderwolf/{date}/{job_id}. A key that would climb out of the prefix is refused when the job is submitted, rather than quietly rewritten into a different key.
An upload is checksummed and verified after writing, and an object already there with the same checksum is left alone - so a retried delivery is a no-op rather than a second write.
Delivery never changes a render
This is the part worth relying on. A job that rendered is succeeded whatever your endpoint did afterwards: a webhook that cannot be reached does not spend a render attempt, hold a render slot, or turn a successful job into a failed one. Deliveries retry on their own ladder, and GET /renderwolf/v1/deliveries shows where each one got to.
If a storage delivery gives up, the webhook destination is told with a render.delivery.failed event naming the destination and the error - which is how you find out that a bucket stopped accepting uploads without watching for absent files.
Recipes
Use these end-to-end guides for common Ironfang Render workflows.
- Generate Open Graph images from reusable templates: store one template, create a signed URL for each page, and update the design without replacing the URLs.
- Generate invoice PDFs from existing HTML: use existing invoice HTML and CSS with print backgrounds, page numbers, headers and footers.
- Screenshot a page behind a login: provide the session with the render request to capture the authenticated page.
- Capture screenshots without ads or cookie banners: block network requests and hide consent overlays without recording consent.
- Screenshots in GitHub Actions: capture deployment previews during pull request workflows for review.
Choose a language quickstart for Python, Node.js, PHP and Go.
GET /v1/usage
Your current consumption against your plan's hard cap:
{"period": "2026-08", "credits": 1240, "renders": 1240, "limit": 50000}renders repeats credits under the name this API shipped with. It is deprecated and will be removed after connector migrations are complete. New integrations should read credits.
Credits
A screenshot or generated image uses one credit, and a PDF uses two credits. Site previews use one credit per output second, with the poster included. Clip usage varies by duration and output size. Most work reserves credits before rendering. Site preview cost is calculated from the captured page height before video encoding. Any charged credits are returned automatically if the work fails. Cache hits and QR requests use zero credits, although QR requests still appear in usage records.
| Plan | Credits per month | Price | Per credit |
|---|---|---|---|
| Free | 250 | Free | Not applicable |
| Hobby | 5,000 | £9 | 0.180p |
| Pro | 15,000 | £19 | 0.127p |
| Scale | 50,000 | £49 | 0.098p |
Higher plans have a lower unit cost. Scale is 46% below Hobby per credit. Reaching the cap pauses work with 429 quota_exhausted and does not create an overage charge. Paid accounts meter usage between billing anniversaries, with period showing the cycle start date. Free accounts meter usage by calendar month.
Badge on the free plan
Screenshots, template images, PDFs and clips created on the free plan include a small Ironfang Render badge in the bottom corner. Every paid plan removes it. Free and paid plans use the same endpoints, rendering fleet and product features.
It is applied from your plan, so there is no request field for it in either direction. Clips put it bottom left, which leaves the bottom right for your own watermark.
Rate limits
Two rate limits operate separately from the monthly plan allowance. Each render can create multiple requests to the target page, so these limits protect both the target site and the Ironfang Render service.
- 60 renders per minute per target host, counted across all customers. Exceeding it returns
429withtarget_rate_limited. - 120 renders per minute per account. Exceeding it returns
429withrate_limited.
Cache hits do not count towards either limit because they send no target traffic. A request blocked by a rate limit does not use credits. Contact Ironfang if you need a higher limit for a site you own.
Response headers
Every rendered asset reports request timings. X-Renderwolf-Render-Ms contains the server-side render time, while X-Renderwolf-Delay-Ms contains the requested delay_ms portion. Cache hits carry X-Renderwolf-Cache: hit, and live captures carry X-Renderwolf-Captured-At.
X-Renderwolf-Credits reports the credits used by each metered endpoint. Log this value to track usage per request without polling /v1/usage. Cache hits report 0.
Caching
Identical render requests served from cache use zero credits. Cache hits carry an X-Renderwolf-Cache: hit header and do not reduce the plan allowance. Assets return Cache-Control: public, max-age=3600, so browsers and CDNs can hold them for an hour. Editing a template invalidates its cache immediately.
Set "no_cache": true when a workflow requires a new capture, such as evidence collection, archiving or change monitoring. This bypasses the cache. The response includes X-Renderwolf-Captured-At with the UTC capture time and Cache-Control: no-store to prevent downstream caching. New captures use the normal credit allowance.
Errors
Every response carries X-Ironfang-Request-ID, and error bodies repeat it as request_id. Quote it when you contact support and we can find the exact request. If you send your own X-Request-ID (printable ASCII, up to 128 characters) it is echoed back unchanged as a separate correlation value.
Every error is JSON with a stable machine-readable code:
{"error": {"code": "quota_exhausted", "message": "monthly credits exhausted - upgrade your plan or wait for the period to reset"}}| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed JSON or invalid parameters. |
| 401 | invalid_api_key | Missing or unknown API key. |
| 403 | bad_signature | Signed URL failed verification or expired. |
| 404 | not_found | The template or signed route does not exist. |
| 422 | render_failed | Chromium could not render the target because of an invalid URL, blocked private address or selector timeout. |
| 429 | quota_exhausted | Monthly cap reached. Upgrade or wait for the period to reset. |
