{"components":{"headers":{"Locale":{"description":"The locale the page formatted with: its `Intl` default, after\n`locale` if one was given (`de-DE`), otherwise the browser's own.\n","schema":{"type":"string"}},"Meter":{"description":"The billing meter this request counted against, e.g.\n`render.screenshot`. Present on every billable response, cache hits\nincluded.\n","schema":{"type":"string"}},"Quantity":{"description":"The quantity this request committed to its meter: 1 for a unit\nrender, output seconds (rounded up) for video, `0` for a cache hit or\nan unmetered key. Read it to log usage per request without polling\n`/v1/usage`.\n","schema":{"type":"integer"}},"Settle":{"description":"How the page settled before capture: `stable` (it went quiet on its\nown), `ceiling` (it was still changing at the settling cap),\n`fixed` (held for the requested `delay_ms`) or `skipped`.\n","schema":{"enum":["stable","ceiling","fixed","skipped"],"type":"string"}},"SettleMs":{"description":"How long settling took, in milliseconds.","schema":{"type":"integer"}},"TargetRedirects":{"description":"How many redirects the page followed, when it followed any.","schema":{"type":"integer"}},"TargetStatus":{"description":"The HTTP status of the page's own final response, after redirects.\nA successful capture of a page that answered 404 is a 200 from this\nAPI with `X-Ironfang-Target-Status: 404`. Absent when there was no\nresponse: raw `html`, or a navigation that got none.\n","schema":{"type":"integer"}},"Timezone":{"description":"The time zone the page's clock ran in: its `Intl` time zone, after\n`timezone` if one was given (`Europe/Berlin`), otherwise `UTC`.\n","schema":{"type":"string"}},"Warnings":{"description":"Comma-separated codes for output that is complete as a render but not\nthe whole page: `height_capped` (a full-page capture was cut at `full_page_max_height`,\nor for WebP where the image would pass 16,383 pixels);\nwith `full_page_readiness`, `scroll_budget_exhausted` (the walk stopped on\nits budget before the bottom of the capture), `images_incomplete` (images\ninside the capture were still loading when the wait ended) and\n`images_broken` (images inside the capture failed to load - the page's\nstate, captured as it is); and `bot_challenge`, when the page is\nrecognisably a bot-verification or challenge interstitial rather than\nthe page asked for (raised only on strong signals; not every challenge\nis recognised). New codes may be added.\n","schema":{"type":"string"}}},"parameters":{"DocumentTemplateId":{"description":"Document template id (UUIDv7).","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},"IdempotencyKey":{"description":"Makes a submission safe to retry. Any string up to 255 characters;\nsurrounding whitespace is ignored and a longer key is a `400\nbad_request`. A key is remembered per account for as long as the job\nit created exists, and has no expiry of its own. Jobs and batches\nkeep separate key spaces.\n\nA job submitted again under its key returns the existing job with\n`200` when the request is the same, and is a `409\nidempotency_conflict` when it differs. A batch submitted again under\nits key returns the batch the key first created, with its jobs and\n`202`; the request is not compared, so a batch has no conflict\nresponse.\n","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}},"RevisionNumber":{"description":"Revision number, from 1.","in":"path","name":"n","required":true,"schema":{"minimum":1,"type":"integer"}},"SyncIdempotencyKey":{"description":"Makes a synchronous render happen, and count, at most once. Any string\nup to 255 characters, remembered per account. The first request under\na key is admitted and rendered as usual; a synchronous result is not\nkept, so the same request again under the key is a `409\nidempotency_key_used` and reserves nothing - send a new key to render\nagain. A different request under a used key is a `409\nidempotency_conflict`. An identical request inside the cache window is\nstill served from cache.\n","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}},"TemplateId":{"description":"Template id (UUIDv7).","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}},"responses":{"BadRequest":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Malformed body, or neither/both of `url` and `html`."},"BillingRefused":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Paid usage cannot be billed: `paid_usage_paused`, `payment_required`,\n`payment_action_required` or `account_restricted`. Resolve it in\nBilling; nothing was reserved or counted. The same body can also\ncarry `account_closed` (410) and `operation_too_large` (413).\n"},"BillingUnavailable":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`billing_temporarily_unavailable` - the billing ledger could not admit\nthe request just now. Retry after `Retry-After` seconds; nothing was\nreserved.\n","headers":{"Retry-After":{"schema":{"type":"integer"}}}},"Bundle":{"content":{"multipart/form-data":{"encoding":{"manifest":{"contentType":"application/json"}},"schema":{"properties":{"manifest":{"$ref":"#/components/schemas/BundleManifest"},"markdown":{"description":"`page.md`, UTF-8: headings, paragraphs, absolute links,\nimages by address, emphasis, code, quotes, lists and\nGitHub-flavoured tables. Scripts, styles, form controls,\nembedded documents and markup hidden with `hidden`,\n`aria-hidden` or an inline `display:none` are left out;\ncontent hidden only by a stylesheet is kept.\n","format":"binary","type":"string"},"rendered_html":{"description":"The rendered DOM serialised with its doctype, `rendered.html`, UTF-8.","format":"binary","type":"string"},"screenshot":{"description":"The screenshot, `screenshot.png`, `.jpeg` or `.webp` by `format`.","format":"binary","type":"string"},"text":{"description":"The page's text as laid out (`innerText` of the body), `text.txt`, UTF-8.","format":"binary","type":"string"}},"required":["manifest"],"type":"object"}}},"description":"The manifest, then each included output as a file part named by\noutput.\n","headers":{"X-Ironfang-Meter":{"$ref":"#/components/headers/Meter"},"X-Ironfang-Quantity":{"$ref":"#/components/headers/Quantity"},"X-Ironfang-Render-Ms":{"schema":{"type":"integer"}}}},"Clip":{"content":{"video/mp4":{"schema":{"format":"binary","type":"string"}}},"description":"The rendered clip.","headers":{"X-Ironfang-Cache":{"schema":{"enum":["hit"],"type":"string"}},"X-Ironfang-Meter":{"$ref":"#/components/headers/Meter"},"X-Ironfang-Quantity":{"$ref":"#/components/headers/Quantity"},"X-Ironfang-Render-Ms":{"schema":{"type":"integer"}}}},"DocumentFailed":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`data_invalid`: the data does not match the revision's schema, with\n`fields` naming each path; nothing was reserved or rendered.\n`template_render_failed`: the template failed while running (a field it\nreads that the data lacks, a function given the wrong kind of value, or\na bound - 100,000 loop iterations, 8 MiB of HTML, 2 s), with the line.\n`template_invalid`: the source does not compile. `render_failed`: the\nbrowser could not print it.\n"},"DocumentPdf":{"content":{"application/pdf":{"schema":{"format":"binary","type":"string"}}},"description":"The rendered PDF.","headers":{"X-Ironfang-Meter":{"$ref":"#/components/headers/Meter"},"X-Ironfang-Quantity":{"$ref":"#/components/headers/Quantity"},"X-Ironfang-Template-Revision":{"description":"The revision number rendered, or `draft` for a preview.","schema":{"type":"string"}}}},"Forbidden":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`insufficient_scope` - the key does not carry the scope;\n`account_not_linked` - the key's account belongs to no Ironfang\norganisation, so its usage cannot be billed; `product_not_eligible` -\npaid use of Ironfang Render needs an eligible billing account.\n"},"IdempotencyConflict":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`idempotency_key_used` - this Idempotency-Key already made its render\n(or is making it, or failed); nothing was reserved again.\n`idempotency_conflict` - the key was used for a different request.\n"},"Image":{"content":{"image/jpeg":{"schema":{"format":"binary","type":"string"}},"image/png":{"schema":{"format":"binary","type":"string"}}},"description":"The rendered image. `png` unless `format: jpeg` was requested.\n","headers":{"X-Ironfang-Cache":{"description":"`hit` when served from cache, in which case it counted nothing.","schema":{"enum":["hit"],"type":"string"}},"X-Ironfang-Captured-At":{"description":"RFC 3339 timestamp of the capture: on a `no_cache` render, where\nthe response is also `no-store` (the pairing is what makes a\ncapture usable as evidence), and on a cache hit, where it is when\nthe cached capture was made.\n","schema":{"format":"date-time","type":"string"}},"X-Ironfang-Delay-Ms":{"description":"The `delay_ms` actually waited, when one was requested.","schema":{"type":"integer"}},"X-Ironfang-Locale":{"$ref":"#/components/headers/Locale"},"X-Ironfang-Meter":{"$ref":"#/components/headers/Meter"},"X-Ironfang-Quantity":{"$ref":"#/components/headers/Quantity"},"X-Ironfang-Render-Ms":{"description":"Render time in milliseconds, excluding any requested delay.","schema":{"type":"integer"}},"X-Ironfang-Settle":{"$ref":"#/components/headers/Settle"},"X-Ironfang-Settle-Ms":{"$ref":"#/components/headers/SettleMs"},"X-Ironfang-Target-Redirects":{"$ref":"#/components/headers/TargetRedirects"},"X-Ironfang-Target-Status":{"$ref":"#/components/headers/TargetStatus"},"X-Ironfang-Timezone":{"$ref":"#/components/headers/Timezone"},"X-Ironfang-Warnings":{"$ref":"#/components/headers/Warnings"}}},"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such template, or not yours."},"Pdf":{"content":{"application/pdf":{"schema":{"format":"binary","type":"string"}}},"description":"The rendered PDF.","headers":{"X-Ironfang-Locale":{"$ref":"#/components/headers/Locale"},"X-Ironfang-Meter":{"$ref":"#/components/headers/Meter"},"X-Ironfang-Quantity":{"$ref":"#/components/headers/Quantity"},"X-Ironfang-Render-Ms":{"schema":{"type":"integer"}},"X-Ironfang-Settle":{"$ref":"#/components/headers/Settle"},"X-Ironfang-Settle-Ms":{"$ref":"#/components/headers/SettleMs"},"X-Ironfang-Target-Redirects":{"$ref":"#/components/headers/TargetRedirects"},"X-Ironfang-Target-Status":{"$ref":"#/components/headers/TargetStatus"},"X-Ironfang-Timezone":{"$ref":"#/components/headers/Timezone"}}},"RateLimited":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limits and exhausted billing allowances share this status,\nseparated 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- `clip_busy`, `preview_busy` - every encoder or preview slot on the\n  worker is in use; `Retry-After` says when to come back\n- `concurrency_limit` - your account already has 2 browser\n  operations in progress, synchronous renders and running jobs\n  together; retry after `Retry-After`, or submit a durable job,\n  which waits for a free slot instead of being refused\n- `queue_full` - your account already has 1,000 unfinished jobs, or\n  the batch would take it past that; nothing was enqueued\n- `free_allowance_exhausted` - this month's free allowance for the\n  meter is used up and paid usage is not enabled\n- `account_budget_exhausted`, `product_budget_exhausted` - the\n  billing account's monthly limit, or Ironfang Render's, is reached\n- `exemption_limit_reached` - an exempt account's agreed limit is reached\n\nThe rate and capacity limits clear by themselves, so retry with\nbackoff, after `Retry-After` where it is sent. The billing codes do\nnot: enable paid usage or raise the limit in Billing, or wait for the\nmonth to roll over. Nothing was reserved or counted. Branch on the\ncode rather than the status.\n","headers":{"Retry-After":{"description":"Seconds to wait before retrying, on the refusals that clear by themselves.","schema":{"type":"integer"}}}},"RenderFailed":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The page did not render: the host did not resolve, it never finished\nloading, it timed out, or it was blocked. Try `delay_ms`, and check the\ntarget loads in a normal browser.\n"},"SitePreview":{"content":{"multipart/form-data":{"schema":{"properties":{"poster":{"description":"JPEG first frame, suitable for a video poster.","format":"binary","type":"string"},"video":{"description":"H.264 MP4 with fast-start metadata.","format":"binary","type":"string"}},"required":["poster","video"],"type":"object"}}},"description":"The poster image and scrolling MP4 returned as two file parts.","headers":{"X-Ironfang-Cache":{"schema":{"enum":["hit"],"type":"string"}},"X-Ironfang-Meter":{"$ref":"#/components/headers/Meter"},"X-Ironfang-Output-Seconds":{"description":"Calculated video length in seconds. Present on fresh renders.","schema":{"type":"number"}},"X-Ironfang-Quantity":{"description":"Output seconds committed, rounded up; the poster is included. `0`\non a cache hit.\n","schema":{"type":"integer"}},"X-Ironfang-Render-Ms":{"schema":{"type":"integer"}}}},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing, malformed, revoked or unknown API key."}},"schemas":{"BatchList":{"additionalProperties":false,"properties":{"batches":{"items":{"$ref":"#/components/schemas/BatchSummary"},"maxItems":100,"type":"array"},"next_cursor":{"type":"string"}},"required":["batches","next_cursor"],"type":"object"},"BatchSummary":{"additionalProperties":false,"properties":{"completed_at":{"format":"date-time","type":["string","null"]},"counts":{"additionalProperties":false,"properties":{"cancelled":{"maximum":100,"minimum":0,"type":"integer"},"failed":{"maximum":100,"minimum":0,"type":"integer"},"queued":{"maximum":100,"minimum":0,"type":"integer"},"running":{"maximum":100,"minimum":0,"type":"integer"},"succeeded":{"maximum":100,"minimum":0,"type":"integer"},"total":{"maximum":100,"minimum":0,"type":"integer"}},"required":["total","queued","running","succeeded","failed","cancelled"],"type":"object"},"created_at":{"format":"date-time","type":"string"},"done":{"type":"boolean"},"external_id":{"type":"string"},"id":{"format":"uuid","type":"string"},"item_count":{"maximum":100,"minimum":1,"type":"integer"}},"required":["id","external_id","item_count","created_at","completed_at","counts","done"],"type":"object"},"BundleManifest":{"properties":{"capture":{"properties":{"finished_at":{"format":"date-time","type":"string"},"page_read_at":{"description":"When HTML","format":"date-time","in one script call.":null,"text and metadata were read":null,"type":"string"},"screenshot_at":{"description":"When the screenshot was taken; absent without one.","format":"date-time","type":"string"},"started_at":{"format":"date-time","type":"string"}},"required":["started_at","page_read_at","finished_at"],"type":"object"},"child_frames":{"description":"Frames whose content is not in the HTML","text or Markdown.":null,"type":"integer"},"metadata":{"description":"Present when `metadata` was requested.","properties":{"canonical_url":{"type":"string"},"description":{"type":"string"},"lang":{"type":"string"},"open_graph":{"additionalProperties":{"type":"string"},"description":"`og:` properties present: title, description, image, type, site_name, url.","type":"object"},"robots":{"type":"string"},"title":{"type":"string"}},"type":"object"},"outputs":{"description":"Every requested output, in order, with what became of it.","items":{"properties":{"bytes":{"type":"integer"},"content_type":{"type":"string"},"filename":{"type":"string"},"limit_bytes":{"type":"integer"},"name":{"enum":["screenshot","rendered_html","text","markdown","metadata"],"type":"string"},"part":{"description":"The multipart part holding it; `manifest` for metadata.","type":"string"},"reason":{"enum":["too_large","source_too_large","unparseable"],"type":"string"},"sha256":{"type":"string"},"source_bytes":{"description":"The full size of an output cut or left out for size","type":"integer","when it was measured.":null},"status":{"enum":["included","truncated","omitted"],"type":"string"}},"required":["name","status"],"type":"object"},"type":"array"},"target":{"description":"The main document's final response; null for `html` input.","properties":{"redirects":{"type":"integer"},"status":{"type":"integer"},"url":{"description":"The final address","query or fragment.":null,"type":"string","without credentials":null}},"type":["object","null"]},"version":{"enum":[1],"type":"integer"}},"required":["version","capture","outputs","child_frames","target"],"type":"object"},"BundleRequest":{"allOf":[{"$ref":"#/components/schemas/ScreenshotRequest"},{"properties":{"outputs":{"description":"The outputs to include. Options that only shape an image\n(`selector`, `clip`, `format`, `quality`, `omit_background`,\n`success_rules.fail_on_blank`) need `screenshot` among them.\n","items":{"enum":["screenshot","rendered_html","text","markdown","metadata"],"type":"string"},"minItems":1,"type":"array","uniqueItems":true}},"required":["outputs"],"type":"object"}]},"Clip":{"description":"A rectangle in CSS pixels from the top left of the page.","properties":{"height":{"maximum":4096,"type":"number"},"width":{"maximum":4096,"type":"number"},"x":{"default":0,"type":"number"},"y":{"default":0,"type":"number"}},"required":["width","height"],"type":"object"},"ClipRequest":{"description":"Everything is optional. With no background a solid `colour` is used,\nwhich is what most captioned clips want.\n","properties":{"audio":{"description":"An audio track, trimmed to the clip's length.","format":"uri","type":"string"},"background":{"description":"An image or video to fill the canvas, cropped to fit.","format":"uri","type":"string"},"captions":{"items":{"properties":{"from":{"description":"Seconds from the start.","type":"number"},"text":{"maxLength":280,"type":"string"},"to":{"type":"number"}},"required":["text","to"],"type":"object"},"maxItems":12,"type":"array"},"colour":{"default":"#101820","description":"Background colour, used when no background asset is given.","pattern":"^#[0-9a-fA-F]{6}$","type":"string"},"duration":{"default":15,"description":"Seconds of output.","maximum":60,"minimum":1,"type":"number"},"font_size":{"description":"Caption size in pixels; defaults to a fifteenth of the width.","maximum":200,"minimum":12,"type":"integer"},"size":{"default":"vertical","description":"vertical 1080x1920, square 1080x1080, landscape 1920x1080,\n720p 1280x720.\n","enum":["vertical","square","landscape","720p"],"type":"string"},"watermark":{"description":"A PNG placed in the bottom right corner.","format":"uri","type":"string"}},"type":"object"},"Cookie":{"properties":{"domain":{"description":"Required. Chromium silently drops a domainless cookie set before\nnavigation, so the render would quietly come back logged out.\n","type":"string"},"name":{"type":"string"},"path":{"default":"/","type":"string"},"value":{"type":"string"}},"required":["name","domain"],"type":"object"},"DeliveryList":{"additionalProperties":false,"properties":{"deliveries":{"items":{"$ref":"#/components/schemas/DeliveryRecord"},"maxItems":100,"type":"array"},"next_cursor":{"maxLength":256,"type":"string"}},"required":["deliveries","next_cursor"],"type":"object"},"DeliveryRecord":{"additionalProperties":false,"properties":{"attempts":{"minimum":0,"type":"integer"},"created_at":{"format":"date-time","type":"string"},"delivered_at":{"format":"date-time","type":"string"},"destination_id":{"format":"uuid","type":"string"},"error":{"type":"string"},"event":{"type":"string"},"id":{"format":"uuid","type":"string"},"job_id":{"format":"uuid","type":"string"},"next_attempt_at":{"format":"date-time","type":"string"},"object_key":{"type":"string"},"payload":{"description":"Last posted body; omitted from lists and after purging.","type":"string"},"payload_at":{"format":"date-time","type":["string","null"]},"payload_retention_days":{"type":"integer"},"payload_signature":{"type":"string"},"payload_timestamp":{"type":"string"},"response_body":{"type":"string"},"response_status":{"type":"integer"},"status":{"enum":["pending","delivering","delivered","failed"],"type":"string"}},"required":["id","job_id","destination_id","event","status","attempts","created_at"],"type":"object"},"Diagnostics":{"description":"What a render learned about the page: on a job, in its webhook and\non a request history row. Synchronous renders give the same as\n`X-Ironfang-*` response headers. Every field is optional; a render\nthat never loaded a page has no diagnostics at all.\n","example":{"capture_ms":96,"navigate_ms":410,"settle":"stable","settle_ms":252,"target_status":404,"target_url":"https://example.com/missing","viewport":{"height":800,"mobile":false,"scale":1,"width":1280}},"properties":{"capture_ms":{"description":"The screenshot or print itself.","type":"integer"},"failed_rule":{"description":"The `success_rules` field that failed the render; absent when none did.","enum":["fail_on_status","require_selector","require_text","forbid_text","fail_on_blank"],"type":"string"},"locale":{"description":"The page's effective `Intl` locale, as `X-Ironfang-Locale`.","type":"string"},"navigate_ms":{"description":"Page setup, navigation and readiness waits, before settling.","type":"integer"},"readiness":{"description":"What a `full_page_readiness` pass did; absent without one.","properties":{"images_broken":{"description":"Visible images inside the capture that failed to load.","type":"integer"},"images_loading":{"description":"Visible images inside the capture still loading at the end.","type":"integer"},"ms":{"type":"integer"},"profile":{"enum":["scroll"],"type":"string"},"steps":{"description":"Viewports the walk scrolled through.","type":"integer"}},"type":"object"},"settle":{"description":"How the page settled before capture.","enum":["stable","ceiling","fixed","skipped"],"type":"string"},"settle_ms":{"type":"integer"},"target_redirects":{"type":"integer"},"target_status":{"description":"The HTTP status of the page's own final response, after\nredirects. Absent for raw `html`, which has no response.\n","type":"integer"},"target_url":{"description":"The final address after redirects, without credentials, query or\nfragment; at most 512 characters.\n","type":"string"},"timezone":{"description":"The page's effective time zone, as `X-Ironfang-Timezone`.","type":"string"},"viewport":{"description":"The effective layout viewport, in CSS pixels.","properties":{"height":{"type":"integer"},"mobile":{"type":"boolean"},"scale":{"type":"number"},"width":{"type":"integer"}},"type":"object"},"warnings":{"description":"Codes for output that is complete as a render but not the whole\npage, as `X-Ironfang-Warnings`: `height_capped`,\n`scroll_budget_exhausted`, `images_incomplete`, `images_broken`,\n`bot_challenge`.\n","items":{"type":"string"},"type":"array"}},"type":"object"},"DocumentOptions":{"description":"How a document template prints, kept with each revision so a revision\nalways prints the same way. As on `POST /pdf`.\n","properties":{"footer_html":{"description":"Chromium print footer template.","maxLength":65536,"type":"string"},"header_html":{"description":"Chromium print header template.","maxLength":65536,"type":"string"},"landscape":{"type":"boolean"},"margin":{"$ref":"#/components/schemas/Margin"},"metadata":{"description":"As on `POST /pdf`; without a title, the document's own `\u003ctitle\u003e` is kept.","properties":{"author":{"maxLength":500,"type":"string"},"keywords":{"maxLength":500,"type":"string"},"subject":{"maxLength":500,"type":"string"},"title":{"maxLength":500,"type":"string"}},"type":"object"},"page_ranges":{"description":"As on `POST /pdf`.","type":"string"},"paper_format":{"enum":["a3","a4","a5","letter","legal","tabloid"],"type":"string"},"paper_height":{"description":"As on `POST /pdf`.","type":"string"},"paper_width":{"description":"As on `POST /pdf`.","type":"string"},"prefer_css_page_size":{"description":"As on `POST /pdf`.","type":"boolean"},"print_background":{"type":"boolean"},"scale":{"maximum":2,"minimum":0.1,"type":"number"}},"type":"object"},"DocumentRenderRequest":{"properties":{"data":{"description":"The document data: at most 1 MiB, 32 levels deep.","type":"object"},"revision":{"description":"The revision number to render; absent or 0 for the published one.","minimum":0,"type":"integer"}},"type":"object"},"DocumentRevision":{"description":"An immutable published version of a template. It never changes.","properties":{"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"number":{"type":"integer"},"options":{"$ref":"#/components/schemas/DocumentOptions"},"schema":{"type":"object"},"sha256":{"description":"The hash of source","schema and options.":null,"type":"string"},"source":{"description":"Absent from lists.","type":"string"},"template_id":{"format":"uuid","type":"string"}},"type":"object"},"DocumentTemplate":{"allOf":[{"$ref":"#/components/schemas/DocumentTemplateInput"},{"properties":{"archived_at":{"description":"Set once archived. Archived templates render only by revision number.","format":"date-time","type":"string"},"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"published_revision":{"description":"The revision unversioned renders use; null until first published.","nullable":true,"type":"integer"},"updated_at":{"format":"date-time","type":"string"}},"type":"object"}]},"DocumentTemplateInput":{"properties":{"name":{"maxLength":200,"type":"string"},"options":{"$ref":"#/components/schemas/DocumentOptions"},"sample_data":{"description":"Data for previews and validation when a request gives none. Keep it synthetic.","type":"object"},"schema":{"description":"The data the template needs, as a strict subset of JSON Schema:\n`type` (object, array, string, number, integer, boolean),\n`properties`, `required`, `items`, `maxItems`, `maxLength`, `enum`,\n`nullable`, `default` and `description`; any other keyword is\nrefused. The top level is an object. Data is checked against it\nbefore anything renders. A required field is never defaulted; an\noptional one missing from the data gets its `default`, or null, so\n`{{if .notes}}` is a test rather than an error. Fields declared\n`integer` are whole numbers, so `{{if gt .days 30}}` works.\n","type":"object"},"source":{"description":"HTML in Go's html/template syntax: `{{.customer.name}}`,\n`{{range .lines}}...{{end}}`, `{{if .notes}}...{{end}}`. Values are\nescaped for where they land - text, attributes, URLs (a\n`javascript:` URL becomes `#ZgotmplZ`), CSS and scripts. Functions:\nthe html/template builtins (`and`, `or`, `not`, `eq`, `ne`, `lt`,\n`le`, `gt`, `ge`, `len`, `index`, `slice`), `fixed N value` (a\nnumber to N decimal places), `mul`, `add`, `upper`, `lower` and\n`printf` (widths at most 64). `{{define}}`, `{{template}}` and\n`{{block}}` are not supported. A field the template reads that the\ndata lacks is an error, never an empty string: declare optional\nfields in the schema. Rendering is bounded: 100,000 loop iterations,\n8 MiB of HTML and 2 s. Saving refuses a source that does not compile.\n","maxLength":262144,"type":"string"}},"required":["name","source"],"type":"object"},"Error":{"example":{"error":{"code":"target_rate_limited","docs":"https://ironfang.com/render/docs#errors","message":"too many renders for that host, try again shortly"},"request_id":"01992f6e-7c1a-7b3e-9d52-4f0a6c8e2b17"},"properties":{"error":{"properties":{"code":{"description":"Stable, safe to branch on. New codes may be added: treat a\ncode you do not recognise as a failure of the class its HTTP\nstatus indicates.\n","enum":["bad_request","unauthorized","invalid_api_key","auth_failed","forbidden","billing_moved","not_found","rate_limited","target_rate_limited","render_failed","success_rule_failed","data_invalid","template_invalid","template_render_failed","template_archived","template_unpublished","too_many_templates","bad_signature","signing_disabled","account_gone","account_not_linked","account_required","admission_failed","bad_destination","bad_target","batch_plan_failed","batch_submit_failed","batch_too_large","bundle_empty","bundle_failed","clip_busy","concurrency_limit","delegated_not_allowed","delivery_disabled","delivery_plan_failed","destination_disabled","idempotency_conflict","idempotency_key_used","insufficient_scope","invalid_token","job_finished","job_plan_failed","job_submit_failed","logo_fetch_failed","pdf_too_long","page_range_empty","preview_busy","queue_full","result_expired","result_not_ready","result_read_failed","secret_failed","site_preview_disabled","store_failed","unknown_destination","unscannable","vault_failed","video_disabled","free_allowance_exhausted","account_budget_exhausted","product_budget_exhausted","paid_usage_paused","payment_required","payment_action_required","account_restricted","account_closed","product_not_eligible","exemption_limit_reached","operation_too_large","billing_temporarily_unavailable"],"type":"string"},"docs":{"description":"Where the error codes are documented.","format":"uri","type":"string"},"message":{"description":"Human-readable. May change; do not match on it.","type":"string"},"rule":{"description":"On `success_rule_failed`, the `success_rules` field that failed\nthe render. The message says why, naming the value.\n","enum":["fail_on_status","require_selector","require_text","forbid_text","fail_on_blank"],"type":"string"},"target_status":{"description":"On `render_failed` and `success_rule_failed`, the HTTP status\nthe page itself answered with before the render failed (for\nexample \"404\" when a selector was not found on a not-found\npage). Absent when the page gave no response.\n","type":"string"}},"required":["code","message"],"type":"object"},"fields":{"description":"Per-field problems, when the request failed on particular fields:\nfor `data_invalid`, each path in the document data\n(`customer.address.city`, `lines[2].quantity`) and what is wrong\nthere, every problem found up to 50.\n","items":{"properties":{"code":{"description":"`required` or `invalid`.","type":"string"},"field":{"type":"string"},"message":{"type":"string"}},"type":"object"},"type":"array"},"request_id":{"description":"The X-Ironfang-Request-ID of the failed request. Quote it to support.","type":"string"}},"required":["error"],"type":"object"},"JobDelivery":{"description":"Where this job goes when it finishes, by destination id. Register the\ndestination first; credentials are never sent here.\n\nA webhook fires on success, failure and cancellation. Storage only\ncarries a result, so it fires on success alone - and a storage\ndelivery that gives up raises `render.delivery.failed` on the webhook\ndestination, which is how you find out that a bucket stopped\naccepting uploads.\n","properties":{"storage_destination":{"description":"Id of an `s3` destination.","type":"string"},"storage_key":{"description":"The object key, relative to the destination's prefix. Fixed text\nplus `{job_id}`, `{external_id}` and `{date}` (`YYYY/MM/DD`) -\nno other field, and nothing evaluated. Segments may hold letters,\ndigits, dot, dash and underscore; anything that would climb out\nof the prefix is refused when the job is submitted, not quietly\nrewritten. Defaults to `render/{date}/{job_id}`.\n","example":"captures/{external_id}/{job_id}.png","maxLength":700,"type":"string"},"webhook_destination":{"description":"Id of a `webhook` destination.","type":"string"}},"type":"object"},"Margin":{"description":"Page margins in inches. Omitted sides keep Chromium's default rather\nthan becoming zero - a PDF printed hard to the paper edge is rarely\nwhat leaving a field out was meant to mean.\n","properties":{"bottom":{"type":"number"},"left":{"type":"number"},"right":{"type":"number"},"top":{"type":"number"}},"type":"object"},"PageAction":{"description":"One ordered pre-capture action. Selectors are CSS selectors in the main\npage. click and hover wait until the first matching element is\ninteractable; wait_for_selector waits for its presence (not visibility).\nAll actions share the remaining timeout_ms render budget.\n","oneOf":[{"additionalProperties":false,"properties":{"selector":{"description":"Non-blank CSS selector; at most 1024 UTF-8 bytes.","maxLength":1024,"minLength":1,"type":"string"},"type":{"enum":["click","hover","wait_for_selector"],"type":"string"}},"required":["type","selector"],"type":"object"},{"additionalProperties":false,"properties":{"duration_ms":{"description":"Delay in milliseconds; all action delays together may total at most 10000 ms.","maximum":10000,"minimum":0,"type":"integer"},"type":{"enum":["delay"],"type":"string"}},"required":["type","duration_ms"],"type":"object"}]},"PdfRequest":{"allOf":[{"$ref":"#/components/schemas/RenderCommon"},{"properties":{"footer_html":{"description":"Chromium print footer template.","type":"string"},"header_html":{"description":"Chromium print header template.","type":"string"},"html":{"description":"Markup to print. Mutually exclusive with `url`.","type":"string"},"landscape":{"default":false,"type":"boolean"},"margin":{"$ref":"#/components/schemas/Margin"},"metadata":{"description":"The PDF's document properties. Each is at most 500 characters,\nUnicode, without control characters. Without a `title` the page's\nown `\u003ctitle\u003e` is kept. Chromium writes the title; the other fields\nare added as a standard PDF incremental update, which leaves the\nprinted document unchanged.\n","properties":{"author":{"maxLength":500,"type":"string"},"keywords":{"maxLength":500,"type":"string"},"subject":{"maxLength":500,"type":"string"},"title":{"maxLength":500,"type":"string"}},"type":"object"},"page_ranges":{"description":"Pages to keep, numbered from 1: pages (`5`), ranges (`1-3`) and\nranges open at the end (`8-`, to the last page), separated by\ncommas. The PDF holds each selected page once, in document order,\nwhatever order or repetition they were named in. Pages past the end\nare ignored; ranges that select no page are a 422 `page_range_empty`.\nThe whole document is printed first and must be within the page\nbound (a 422 `pdf_too_long` otherwise), so a small range never\nrenders an unbounded document. It counts as one PDF, as any PDF does.\n","example":"1-3, 5, 8-","type":"string"},"paper_format":{"description":"Named page size. Omit for Chromium's default, which is Letter.\nSizes are portrait; `landscape` rotates them.\n","enum":["a3","a4","a5","letter","legal","tabloid"],"type":"string"},"paper_height":{"description":"The custom page height, as `paper_width`.","example":"297mm","type":"string"},"paper_width":{"description":"A custom page width with its unit: `in`, `cm`, `mm`, `pt` or\n`px` (CSS: 96 px and 72 pt an inch), such as `210mm` or\n`8.5in`. Give it with `paper_height`, and not with\n`paper_format`: two sizes are refused, not reconciled. Each side\nis 1 to 100 inches. The page size, in order: a page's own\n`@page { size }` when `prefer_css_page_size` is set; otherwise\n`paper_width`/`paper_height`; otherwise `paper_format`; otherwise\nLetter. `landscape` rotates whichever of the last three applies.\n","example":"210mm","type":"string"},"prefer_css_page_size":{"default":false,"description":"Let the page's own CSS `@page { size: ... }` decide the page size\nwhen it declares one. `landscape` does not rotate a CSS size.\nWithout an `@page` size, the request's size applies as usual.\n","type":"boolean"},"print_background":{"default":false,"description":"Include background colours and images. Off by default, matching\na browser's print dialog.\n","type":"boolean"},"scale":{"description":"Print scale. Omit or `0` for 1.0.","type":"number"},"success_rules":{"$ref":"#/components/schemas/SuccessRules"},"url":{"description":"Page to print. Mutually exclusive with `html`.","format":"uri","type":"string"}},"type":"object"}]},"QrRequest":{"properties":{"dark":{"default":"#000000","description":"Module color, `#rgb` or `#rrggbb`. Must actually be darker than `light`.","type":"string"},"data":{"description":"The payload - a URL, WiFi string, or any text up to 1KB.","maxLength":1024,"type":"string"},"dots":{"default":"square","description":"Module style. `circle` draws separated dots. Finder patterns\nalways render solid - styled finders are how QR generators\nproduce codes that do not scan.\n","enum":["square","circle"],"type":"string"},"ecc":{"default":"M","description":"Error-correction level. Forced to `H` when a logo is present -\nthe logo destroys the modules it covers, and that budget has to\ncome from somewhere.\n","enum":["L","M","Q","H"],"type":"string"},"eyes":{"description":"Finder-pattern style. Defaults to match `dots`.","enum":["square","rounded"],"type":"string"},"invert":{"default":false,"description":"Light modules on a dark ground. Explicit because it narrows the\naudience: modern phone cameras read inverted codes, many\nembedded and in-app scanners do not - use it for screens, not\nprint or payments. With the flag set, `dark` (the module color)\nmust be lighter than `light` (the ground). Verified against the\nnegative, so the structural guarantee holds.\n","type":"boolean"},"light":{"default":"#ffffff","description":"Background color, or `transparent`. Transparent codes are\nverified against a white ground - place them on light surfaces.\n","type":"string"},"logo":{"description":"Center mark as a base64 `data:` URI, png or jpeg, up to 1MB\ndecoded. Mutually exclusive with `logo_url`.\n","type":"string"},"logo_pad":{"default":true,"description":"Knockout tile behind the logo so it sits on clean ground.","type":"boolean"},"logo_size":{"default":0.22,"description":"Logo tile as a fraction of the symbol width.","maximum":0.3,"minimum":0.12,"type":"number"},"logo_url":{"description":"Center mark by URL. SSRF-guarded and rate-capped like a render target.","format":"uri","type":"string"},"margin":{"default":4,"description":"Quiet zone, in modules.","maximum":8,"minimum":0,"type":"integer"},"size":{"default":512,"description":"Output side in pixels. Output is always square PNG.","maximum":2048,"minimum":64,"type":"integer"}},"required":["data"],"type":"object"},"RenderCommon":{"properties":{"actions":{"description":"Up to 20 actions, executed in order after css and script, before\nsettling and capture. Supports screenshots and PDFs. Missing or\nunready selectors and action timeouts fail with 422 render_failed,\nidentifying the zero-based action index and type. Invalid action\nshapes are 400 bad_request. An action failure stops the list and is\nnot retried in queued jobs. All options, including action order,\nare part of the screenshot cache key; use no_cache for a fresh run.\n","example":[{"selector":"#show-more","type":"click"},{"selector":"#details","type":"wait_for_selector"}],"items":{"$ref":"#/components/schemas/PageAction"},"maxItems":20,"type":"array"},"authorization":{"description":"Sets the `Authorization` header - the common case, spelled once.\nAn explicit `Authorization` in `headers` wins over this.\n","type":"string"},"block_ads":{"default":false,"description":"Drop requests to known ad and tracker networks before they load.\nStopped as network requests rather than hidden afterwards, so the\npage never spends time fetching them.\n","type":"boolean"},"block_cookie_banners":{"default":false,"description":"Hide common consent banners, and undo the scroll lock they set -\nwithout which a full-page capture is one viewport tall.\n\nHidden, not accepted. Clicking \"accept\" would be a decision made\non the site owner's behalf and recorded as theirs.\n","type":"boolean"},"cookies":{"description":"Cookies to set before navigating. Set beforehand because a session\ncookie that arrives after load has missed the request it was meant\nto authenticate.\n","items":{"$ref":"#/components/schemas/Cookie"},"type":"array"},"css":{"description":"Custom stylesheet, at most 65536 UTF-8 bytes. Applied after normal\nload/wait conditions and built-in hiding, before script, actions,\nsettling and capture. Supports screenshots and PDFs, including\n@media print rules. Malformed stylesheet syntax is a 400\nbad_request; Chromium determines supported properties and values.\n","example":".chat-widget { display: none !important; }","maxLength":65536,"type":"string"},"device_scale_factor":{"description":"Pixel density. 2 is retina: the same CSS size at twice the pixels.\n","maximum":3,"minimum":1,"type":"number"},"headers":{"additionalProperties":{"type":"string"},"description":"Extra HTTP headers sent with every request for the page.","type":"object"},"hide_selectors":{"description":"CSS selectors to hide before capturing. Applied after load, so\nelements injected by script are covered.\n","items":{"type":"string"},"type":"array"},"locale":{"description":"The browser's language, a BCP 47 tag such as `en-GB` or `de-DE`,\nset before the page's scripts run: the default for `Intl` dates,\nnumbers and currencies, `navigator.language` and\n`navigator.languages`, and the `Accept-Language` the page and its\nresources are requested with (`de-DE,de;q=0.9`). An\n`Accept-Language` in `headers` is yours and is sent exactly as\ngiven; `navigator` then keeps the browser's default, while `Intl`\nstill follows `locale`. It does not change where the request comes\nfrom: Render does not route through other countries.\n","examples":["de-DE"],"maxLength":35,"type":"string"},"no_cache":{"default":false,"description":"Force a fresh render instead of serving a cached one. Always\nbillable. Excluded from the cache key, so repeated fresh requests\ndo not collide.\n","type":"boolean"},"script":{"description":"JavaScript statements executed inside the target page's main-frame\ncontext after load/wait conditions and css, before actions and\ncapture. Runs as an async function body: use await or return a\npromise for asynchronous work. Completion is awaited; return values\nare discarded. At most 16384 UTF-8 bytes and 5000 ms execution,\nfurther bounded by the remaining timeout_ms budget. Syntax errors,\nexecution failures and timeouts return 422 render_failed with a\nsanitized message. Supports screenshots and PDFs; no browser-session\nor Playwright/Puppeteer APIs are exposed.\n","example":"document.querySelector('#pricing')?.click();","maxLength":16384,"type":"string"},"timeout_ms":{"description":"Per-render timeout. Clamped to the service maximum - it can lower\nthe ceiling but not raise it.\n","type":"integer"},"timezone":{"description":"The browser's clock, an IANA time zone such as `Europe/London` or\n`America/New_York`, set before the page's scripts run: `Date`,\n`Intl.DateTimeFormat` and everything that reads local time, with\nthe zone's own daylight saving. Default `UTC`. It does not set\nthe browser's geolocation.\n","examples":["Europe/Berlin"],"maxLength":64,"type":"string"},"user_agent":{"description":"Overrides the user agent, including any set by `device`.\n","type":"string"},"wait_for_selector":{"description":"Wait for this element before capturing. Fails the render if it\nnever appears, rather than returning a half-drawn page.\n","type":"string"},"wait_until":{"default":"load","description":"When the page counts as ready. `networkidle` waits for traffic to\nstop and is bounded by the render timeout, because a page with\npolling never truly idles.\n","enum":["load","domcontentloaded","networkidle"],"type":"string"}},"type":"object"},"RenderTemplateRequest":{"allOf":[{"$ref":"#/components/schemas/RenderCommon"},{"properties":{"format":{"default":"png","enum":["png","jpeg","webp"],"type":"string"},"vars":{"additionalProperties":{"type":"string"},"description":"Values for the template's placeholders.","type":"object"}},"type":"object"}]},"ScreenshotRequest":{"allOf":[{"$ref":"#/components/schemas/RenderCommon"},{"properties":{"clip":{"$ref":"#/components/schemas/Clip"},"dark_mode":{"default":false,"description":"Render with `prefers-color-scheme: dark`.","type":"boolean"},"delay_ms":{"description":"Extra settle time after load. Ironfang Render watches the page and\ncaptures when it stops changing, so this is rarely needed - use\nit for content that arrives on a timer, which quiescence cannot\nanticipate. Capped at ten seconds.\n","maximum":10000,"minimum":0,"type":"integer"},"device":{"description":"Capture as a phone or tablet rather than a desktop window that\nhappens to be narrow. Sets the viewport, the device pixel ratio,\ntouch, and a matching mobile user agent together - width alone\ngets media queries right and everything else wrong.\n\nAn explicit `width` or `height` still wins, so a preset can be\nused for the density and user agent while overriding the size.\n","enum":["desktop","tablet","mobile"],"type":"string"},"format":{"default":"png","description":"PNG is lossless and the default. WebP is typically a fraction of\nthe size for the same picture, which is what matters when the\noutput is served to browsers or social crawlers.\n\nA WebP image is at most 16,383 pixels on a side. A `full_page`\nWebP capture is cut there - at the device scale, 5,461 CSS\npixels on the mobile preset - and warns `height_capped`; a\n`selector` capture larger than that is refused. Use `jpeg` or\n`png` for longer pages.\n","enum":["png","jpeg","webp"],"type":"string"},"full_page":{"default":false,"description":"Capture the whole scrollable page.","type":"boolean"},"full_page_max_height":{"default":20000,"description":"Cap a `full_page` capture at this many pixels. An\ninfinite-scroll page has no natural end, so 20000 applies when\nnone is given, and is also the most that can be asked for.\n","maximum":20000,"type":"integer"},"full_page_readiness":{"description":"`scroll` walks a `full_page` capture down before taking it, a\nviewport at a time with overlap, so content a page loads only\nas it comes into view - `loading=lazy` images,\nIntersectionObserver loaders - is in the picture without a\nscript of your own. Each step waits up to 750 ms for the\nimages it exposed; the walk then returns to the top and waits\nup to 2 s for the images inside the capture. The whole pass\ntakes at most 10 s (less when `timeout_ms` leaves less, keeping\n3 s for the capture), at most 60 steps, and never goes below\n`full_page_max_height`, so an infinite feed ends where the\ncapture is cut.\n\nThe page keeps its authored layout: the capture is taken from\nthe top, so a sticky header appears once, at the top, and a\nfixed element where it sits in the first viewport. What the\npass did is in the `X-Ironfang-Warnings` header and a job's\n`diagnostics.readiness`; a budget that runs out is a warning\non a successful capture, not a failure. Omitted, the capture\nis taken as it always has been. Requires `full_page`.\n","enum":["scroll"],"type":"string"},"height":{"default":800,"description":"Viewport height. Ignored when `full_page` is set.","maximum":4096,"type":"integer"},"html":{"description":"Markup to render. Mutually exclusive with `url`.","type":"string"},"omit_background":{"default":false,"description":"Transparent background instead of the page's own. Ignored for\n`jpeg`, which has no alpha channel.\n","type":"boolean"},"quality":{"default":85,"description":"Quality for the lossy formats, `jpeg` and `webp`. Ignored for PNG.","maximum":100,"minimum":1,"type":"integer"},"selector":{"description":"CSS selector of a single element to capture instead of the\nviewport.\n","type":"string"},"success_rules":{"$ref":"#/components/schemas/SuccessRules"},"url":{"description":"Page to capture. Mutually exclusive with `html`.","format":"uri","type":"string"},"width":{"default":1280,"description":"Viewport width in pixels.","maximum":4096,"type":"integer"}},"type":"object"}]},"SignRequest":{"properties":{"full_page":{"default":false,"type":"boolean"},"full_page_readiness":{"description":"Screenshot only, with `full_page`; as on the screenshot request. Signed into the URL.","enum":["scroll"],"type":"string"},"height":{"maximum":4096,"type":"integer"},"kind":{"description":"`screenshot` signs a URL capture and requires `url`; `image` signs a\ntemplate render and requires `template`.\n","enum":["screenshot","image"],"type":"string"},"locale":{"description":"Screenshot only; as on the screenshot request. Signed into the URL.","type":"string"},"success_rules":{"$ref":"#/components/schemas/SuccessRules"},"template":{"description":"Required when `kind` is `image`.","format":"uuid","type":"string"},"timezone":{"description":"Screenshot only; as on the screenshot request. Signed into the URL.","type":"string"},"ttl_hours":{"default":720,"description":"Hours until the URL expires. Leaving it out gives 720 (30 days);\n`0` never expires.\n","minimum":0,"type":"integer"},"url":{"description":"Required when `kind` is `screenshot`.","format":"uri","type":"string"},"vars":{"additionalProperties":{"type":"string"},"type":"object"},"width":{"maximum":4096,"type":"integer"}},"required":["kind"],"type":"object"},"SignedUrl":{"properties":{"expires_at":{"description":"When the URL stops rendering (UTC); null when it never expires.","format":"date-time","type":["string","null"]},"path":{"description":"Path only, for serving behind your own domain.","type":"string"},"url":{"description":"Absolute URL, ready to embed.","format":"uri","type":"string"}},"type":"object"},"SitePreviewRequest":{"allOf":[{"$ref":"#/components/schemas/RenderCommon"},{"properties":{"dark_mode":{"default":false,"description":"Record with `prefers-color-scheme: dark`.","type":"boolean"},"delay_ms":{"description":"Extra settle time after page load and before lazy-content preloading. Capped at ten seconds.","maximum":10000,"minimum":0,"type":"integer"},"device":{"description":"Use a full device preset. Explicit width and height still win.\n","enum":["desktop","tablet","mobile"],"type":"string"},"height":{"default":494,"description":"Output and browser viewport height in pixels. Width multiplied\nby height cannot exceed 1,200,000 pixels.\n","maximum":1200,"minimum":240,"multipleOf":2,"type":"integer"},"motion":{"default":"per_page","description":"`per_page` moves one viewport at a time with eased transitions\nand a 750ms reading pause. `single_sweep` makes one\ncontinuous eased pass from top to bottom.\n","enum":["per_page","single_sweep"],"type":"string"},"no_cache":{"default":false,"description":"Force a fresh recording instead of using a cached result.","type":"boolean"},"url":{"description":"Public HTTP or HTTPS page to record.","format":"uri","type":"string"},"width":{"default":672,"description":"Output and browser viewport width in pixels.","maximum":1280,"minimum":320,"multipleOf":2,"type":"integer"}},"required":["url"],"type":"object"}]},"SuccessRules":{"description":"Opt-in rules for output that should count as a failure. A render that\nproduces bytes is a success to the browser whatever the page said; with\nrules, a captured error page, login wall or empty shell fails instead,\nas `422 success_rule_failed` naming the rule (`error.rule`), and - like\nevery failed render - is not charged. Without them a render behaves as\nit always has. Rules are part of the cache identity, so a cached capture\nis only served to a request with the same rules. A job a rule fails\nfails at once, without retries. Signed URLs carry them signed.\n\nWhen each runs: `fail_on_status` once the page has answered, on its\nfinal status after redirects, even when the page then failed to become\nready; the selector and text rules once the page is ready (waits,\nsettling, actions and `full_page_readiness` done), before the capture,\non the main frame; `fail_on_blank` after the capture, on its pixels.\n","example":{"fail_on_status":["4xx","5xx"],"forbid_text":["Sign in to continue"],"require_selector":"#invoice"},"properties":{"fail_on_blank":{"default":false,"description":"Screenshots only. Fails a capture that is a single flat colour: at\nmost one pixel in ten thousand differs from the most common colour\n(transparent counts as a colour). A plain page with any text, a\ncanvas drawing or an image is not blank; neither is a spinner on\nwhite. Judged on a reduced copy of the captured region, adding\nroughly 50-100 ms.\n","type":"boolean"},"fail_on_status":{"description":"Target statuses that fail the render: a code (`404` or `\"404\"`),\na class (`\"5xx\"`) or an inclusive range (`\"400-499\"`). Needs `url`:\nraw `html` has no status.\n","example":["4xx","5xx"],"items":{"oneOf":[{"type":"integer"},{"type":"string"}]},"maxItems":10,"type":"array"},"forbid_text":{"description":"Text that must not be on the page, matched as for `require_text`.","items":{"maxLength":200,"type":"string"},"maxItems":10,"type":"array"},"require_selector":{"description":"A CSS selector that must match an element in the ready page.\nUnlike `wait_for_selector`, it does not wait: combine the two to\nwait for an element and fail when it never comes.\n","maxLength":1024,"type":"string"},"require_text":{"description":"Text that must be on the page. Matched as a literal against the\npage's displayed text (hidden elements do not count), ignoring\ncase and treating any run of whitespace as one space.\n","items":{"maxLength":200,"type":"string"},"maxItems":10,"type":"array"}},"type":"object"},"Template":{"properties":{"created_at":{"format":"date-time","type":"string"},"height":{"type":"integer"},"html":{"type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"width":{"type":"integer"}},"type":"object"},"TemplateInput":{"properties":{"height":{"default":800,"type":"integer"},"html":{"description":"HTML with `{{placeholder}}` markers.","type":"string"},"name":{"type":"string"},"width":{"default":1280,"type":"integer"}},"required":["name","html"],"type":"object"},"TemplateList":{"additionalProperties":false,"properties":{"next_cursor":{"description":"Cursor for the next page; empty on the final page.","type":"string"},"templates":{"items":{"$ref":"#/components/schemas/Template"},"maxItems":100,"type":"array"},"total":{"description":"How many templates the account has in total, independent of this page. Read it rather than counting pages.","minimum":0,"type":"integer"}},"required":["templates","next_cursor","total"],"type":"object"},"Usage":{"description":"This UTC calendar month's usage of every Ironfang Render meter, from\nthe organisation's billing account. Amounts are pounds before tax, as\nexact decimal strings.\n","properties":{"catalogue_version":{"description":"The billing catalogue version that prices this period.","type":"string"},"currency":{"enum":["GBP"],"type":"string"},"meters":{"items":{"properties":{"billable":{"description":"`used` beyond the free allowance.","type":"integer"},"charge":{"before tax.":null,"description":"What `billable` costs","type":"string"},"exempt":{"description":"The account is exempt from settling this product.","type":"boolean"},"free_allowance":{"description":"This month's free quantity.","type":"integer"},"free_used":{"description":"How much of `used` the free allowance covered.","type":"integer"},"free_utility":{"description":"A meter that is never charged (QR codes).","type":"boolean"},"id":{"description":"The meter, e.g. `render.screenshot`.","type":"string"},"remaining_free":{"description":"Free allowance left once work in flight is counted.","type":"integer"},"reserved":{"description":"Quantity held by work in flight.","type":"integer"},"unit":{"type":"string"},"unit_plural":{"type":"string"},"used":{"description":"Quantity committed this month.","type":"integer"}},"type":"object"},"type":"array"},"mode":{"description":"The billing account's mode; `enabled` means paid usage beyond the free allowances is on.","enum":["free","setup_pending","enabled","paused","closed"],"type":"string"},"period_end":{"description":"The period as [period_start, period_end) in UTC dates.","format":"date","type":"string"},"period_start":{"format":"date","type":"string"},"product":{"enum":["render"],"type":"string"},"reserved":{"description":"The most work in flight could add if it all succeeds; not charged on top of `settled`.","type":"string"},"settled":{"description":"What Ironfang Render's settled usage costs this period.","type":"string"}},"type":"object"}},"securitySchemes":{"apiKey":{"description":"An API key from the portal, sent as `Authorization: Bearer if_live_...` (the `Bearer ` prefix is optional).\n","scheme":"bearer","type":"http"}}},"externalDocs":{"description":"Ironfang Render documentation","url":"https://ironfang.com/render/docs"},"info":{"contact":{"email":"hello@ironfang.com","name":"Ironfang","url":"https://ironfang.com"},"description":"Ironfang Render renders web pages to images, PDFs and video. Give it a URL or a\nblock of HTML and get the finished asset without running a browser.\n\n## No attribution on your output\n\nNothing you render through the API carries an Ironfang badge or watermark,\nwhatever your billing state. Only the anonymous free tools on\nironfang.com mark their output.\n\n## Authentication\n\nEvery request carries an API key as a bearer token:\n\n```\nAuthorization: Bearer if_live_...\n```\n\nThe `Bearer ` prefix is optional: `Authorization: if_live_...` works the\nsame, for tools that send the header value exactly as it was pasted. Keys\nminted before 27 August 2026 carry an older prefix and keep working.\n\nKeys are created in the portal at https://portal.ironfang.com. They are shown\nonce at creation and stored only as a hash, so a lost key is replaced rather\nthan recovered.\n\n## Responses are bytes, not JSON\n\n`/v1/screenshot`, `/v1/pdf`, `/v1/image/{id}` and `/v1/video` return the\nrendered file directly. `/v1/site-preview` returns a multipart payload with\nthe poster and MP4. Only errors and the metadata endpoints return JSON.\n\n## Billing\n\nIronfang Render has no plans or credits. Usage is billed to your\norganisation's Ironfang billing account, which every product shares\n(`/billing/v1`, managed in the portal). Each kind of work counts against a\nmeter, and each meter has a monthly free allowance (UTC calendar months);\nbeyond it, usage is pay-as-you-go at graduated rates once paid usage is\nenabled on the billing account. The catalogue of meters, allowances and\nrates is `GET /billing/v1/catalogue`.\n\n| Work | Meter | Counts |\n| --- | --- | --- |\n| Screenshot, signed screenshot | `render.screenshot` | 1 per screenshot |\n| PDF | `render.pdf` | 1 per PDF of at most 100 pages |\n| Template image, signed image | `render.image` | 1 per image |\n| QR code | `render.qr` | 1 per code; a free utility, never charged |\n| Clip, 1080p (`vertical`, `landscape`) | `render.video_second_fhd` | output seconds, rounded up per clip |\n| Clip, `720p` or `square` | `render.video_second_hd` | output seconds, rounded up per clip |\n| Site preview | `render.site_preview_second` | output seconds, rounded up per preview |\n| Capture bundle | `render.screenshot` | 1 per bundle, whatever outputs it holds |\n\nEvery billable request is admitted before any work starts: the most it\ncan count is reserved, and a request that cannot be admitted is refused\nwith nothing done. What the work actually produced is committed when it\nsucceeds - never more than was reserved - and a failure, a refused input\nor a cancellation commits nothing. A clip reserves its duration; a site\npreview reserves the 60 second limit and commits its real length. Billable\nresponses say what they counted in `X-Ironfang-Meter` and\n`X-Ironfang-Quantity`; `GET /v1/usage` reports the period.\n\nA billing refusal answers with a stable code in the usual error body:\n`free_allowance_exhausted`, `account_budget_exhausted` or\n`product_budget_exhausted` (429); `paid_usage_paused`, `payment_required`,\n`payment_action_required` or `account_restricted` (402);\n`product_not_eligible` (403); `account_closed` (410);\n`operation_too_large` (413); `billing_temporarily_unavailable` (503, with\n`Retry-After`). An API key on an account that belongs to no Ironfang\norganisation is `403 account_not_linked`.\n\nA PDF may have at most 100 pages. A longer document is refused with\n`422 pdf_too_long` and nothing is counted.\n\n## Caching\n\nAn identical request made again within **ten minutes** of the render that\nproduced it may be served from cache: `X-Ironfang-Cache: hit`,\n`X-Ironfang-Quantity: 0`, nothing reserved and nothing counted. The cache\nis per instance and bounded in size, so an entry can be evicted sooner and\na hit is never guaranteed; after ten minutes the same request is a new,\nbillable render. PDFs are never cached. Set `no_cache: true` to force a\nfresh render, which is billable; the flag is deliberately excluded from\nthe cache key, so two fresh requests do not hit each other's results.\n\n## Rate limits\n\nTwo rate limits apply, both per minute:\n\n- **120 renders per account**, and\n- **60 renders per target host**, counted across all customers.\n\nThe second protects the sites being rendered: one API call is a full page\nload, so a victim's exposure must not scale with our customer count. Both\nare checked after the cache and before admission, so a blocked call\nreserves and costs nothing.\n\n## Capacity limits\n\nThree more limits are the same for every account, and no billing setting\nlifts them. They are refused with Ironfang Render's own codes, never a\nbilling code, and a refused call reserves and costs nothing.\n\n- **2 browser operations in progress per account.** Screenshots, PDFs,\n  template images and site previews count together, however they\n  arrived: synchronous requests (signed URLs included) and durable jobs a\n  worker is running, across the whole service. A synchronous request\n  over the limit is `429 concurrency_limit` with `Retry-After`; a signed\n  URL, whose caller is a browser or a link-preview crawler, first waits\n  up to 10 seconds for one to finish. A durable job over it is not\n  refused: it waits in the queue until one of the account's operations\n  finishes. QR codes and clips use no browser and do\n  not count. A cache hit uses no browser either.\n- **1,000 unfinished jobs per account** (queued, running or being\n  cancelled). A job or a batch that would take the account past it is\n  `429 queue_full`, with `Retry-After`; a batch is refused whole, and no\n  job of it is created.\n\n## Bot protection\n\nIronfang Render does not attempt to circumvent bot protection, CAPTCHAs or\npaywalls. A challenge page renders as the challenge page. This is a\ndeliberate limit, not a bug.\n\n## For agents and tools\n\nThe canonical base URL is https://api.ironfang.com/render. Every operation\nhas a stable `operationId`. Every error an operation returns is JSON of the\nshape `{\"error\": {\"code\", \"message\", \"docs\"}, \"request_id\"}`: branch on\n`code` and the HTTP status, never on `message`. The machine-readable index\nof the Ironfang APIs is https://ironfang.com/.well-known/api-catalog, and\nhttps://ironfang.com/llms.txt is a plain-text map of them for a language\nmodel. Ironfang Render is also available as MCP tools at\nhttps://mcp.ironfang.com/mcp (see https://ironfang.com/docs/mcp). Renderwolf\nis the name the product launched under, not a separate product; its\n`/renderwolf` prefix and `X-Renderwolf-*` headers were retired on\n28 September 2026, and the prefix now answers `410`.\n","license":{"name":"Proprietary - use governed by the Ironfang terms of service","url":"https://ironfang.com/legal/terms"},"summary":"Screenshots, PDFs, dynamic images and video through one API.","termsOfService":"https://ironfang.com/legal/terms","title":"Ironfang Render API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/v1/batches":{"get":{"description":"Tenant-scoped metadata, newest first by creation time and ID. Equal timestamps retain stable pagination. Newer insertions do not appear on subsequent pages; refresh the first page to see them. No input payloads or child job bodies in batch summaries. A final page has an empty next_cursor. Responses are no-store. The JWT portal twin has the same response and pagination contract.","operationId":"listBatches","parameters":[{"in":"query","name":"limit","schema":{"default":50,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Pass next_cursor from the preceding page; empty means the first page.","in":"query","name":"cursor","schema":{"maxLength":256,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchList"}}},"description":"One bounded page."},"400":{"description":"Malformed cursor, repeated/unknown parameter or limit outside 1–100."},"401":{"description":"Authentication required."},"403":{"description":"The caller cannot read this collection."},"500":{"description":"Storage unavailable."}},"summary":"List batches","tags":["Batches"]},"post":{"description":"One `default` request plus `items` that override parts of it, merged\none level deep: an item's field wins, everything else comes from the\ndefault. The default is written for the batch's `kind`, so it applies\nonly to items of that kind; an item that names a different kind stands\non its own request, and any item may carry its own `delivery`. Every item is validated as a single job would be,\nand an unknown field is refused rather than ignored, because a typo\nthat is silently dropped is a render spent on the wrong thing.\n\nThe batch is accepted or refused whole. Every item is validated before\nany is stored, and every item is admitted on your billing account in\none step, so a batch any item of which cannot be admitted is refused\nwith the billing code, reserves nothing and leaves no jobs behind.\nEach item then commits or releases on its own as its job finishes.\nA validation error names the item that was wrong. A batch that would\ntake the account past 1,000 unfinished jobs is refused whole the\nsame way, with `429 queue_full`.\n\nItems are ordinary jobs: poll them individually, or poll the batch for\nthe aggregate. There is no ZIP of results - configure a storage\ndestination if you want the output collected in one place.\n\nSend `Idempotency-Key` to make the submission safe to retry: the same\nkey returns the batch it first created, with its jobs and another\n`202`, instead of a second set of jobs and a second reservation. The\nrequest is not compared with the first. Poll the batch for its\ncurrent counts.\n","operationId":"submitBatch","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"default":{"description":"The request every item starts from.","type":"object"},"delivery":{"$ref":"#/components/schemas/JobDelivery"},"external_id":{"maxLength":120,"type":"string"},"items":{"items":{"properties":{"delivery":{"$ref":"#/components/schemas/JobDelivery"},"external_id":{"maxLength":120,"type":"string"},"kind":{"type":"string"},"request":{"type":"object"}},"type":"object"},"maxItems":100,"minItems":1,"type":"array"},"kind":{"enum":["screenshot","pdf","qr","image","clip","site_preview","document","bundle"],"type":"string"}},"required":["items"],"type":"object"}}},"required":true},"responses":{"202":{"description":"The batch was accepted; the body carries the batch and one job per item."},"400":{"description":"Invalid submission, or `batch_too_large`. The message names the item."},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"description":"The key carries no `render:render` scope, or is not account-backed."},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Submit up to 100 jobs together","tags":["Batches"]}},"/v1/batches/{id}":{"get":{"description":"`counts` totals the items by state and `done` is true once none of\nthem can change. `jobs` is the full job resource for every item.\n","operationId":"getBatch","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The batch, its counts and its jobs."},"404":{"description":"No such batch on this account."}},"summary":"Poll a batch","tags":["Batches"]}},"/v1/bundle":{"post":{"description":"Loads a page once and returns the outputs you select from that one\nnavigation: a `screenshot`, the `rendered_html` (the DOM after the\npage's scripts, readiness and actions, serialised - not the source\nthe server sent), the page's `text` as the browser lays it out,\ndeterministic `markdown` made from the rendered HTML, and the page's\n`metadata` (title, description, language, canonical address, robots\nand Open Graph). Every screenshot option applies: device, full page,\nreadiness, actions, success rules, request shaping.\n\n**The capture window.** After readiness, actions and success rules,\nthe page is read in one script call - HTML, text and metadata\ntogether, so they describe one state of the document - and the\nscreenshot is taken immediately after. A page that changes itself (a\ntimer, an animation, live data) can change between the two; the\nmanifest gives both times. A bundle is the page as the browser had\nit, not an atomic snapshot of the site's data.\n\n**Frames.** HTML, text and Markdown come from the main frame. The\ncontent of iframes, same origin or not, is not included;\n`child_frames` in the manifest counts the frames left out. The\nscreenshot shows what was painted, frames included.\n\n**Bounds.** Rendered HTML up to 5 MiB, text and Markdown up to 1 MiB\neach, metadata fields up to 2,000 characters. Text and Markdown over\ntheir bound are cut at a character boundary and marked `truncated`;\nHTML over its bound is `omitted` with `too_large` (cutting HTML would\ngive a different document), and so is the Markdown made from it\n(`source_too_large`). A page nested too deeply to parse has its\nMarkdown `omitted` with `unparseable`. A bundle with none of its\noutputs left is a 422 `bundle_empty` and counts nothing. An output is\nnever left out without the manifest saying so.\n\n**The response** is `multipart/form-data`: `manifest` (JSON) first,\nthen each included output as a file part named by output, in the\norder screenshot, rendered_html, text, markdown. Metadata is in the\nmanifest. The manifest gives each output's size and SHA-256.\nCaptured HTML and text are the page's own content: treat them as\nuntrusted, and never insert the HTML into a page of your own.\n\n**Metering.** One bundle counts one `render.screenshot` when it\nsucceeds, whatever outputs it holds; a failure counts nothing.\nBundles are never cached. Also available as a job of kind `bundle`,\nwhose result is the same multipart body.\n\nThis endpoint is available on the product-partitioned URL only.\n","operationId":"createBundle","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"examples":{"everything":{"summary":"Every output of a full page","value":{"full_page":true,"outputs":["screenshot","rendered_html","text","markdown","metadata"],"url":"https://example.com"}},"readable":{"summary":"Markdown and metadata only","value":{"outputs":["markdown","metadata"],"url":"https://example.com/blog/post"}}},"schema":{"$ref":"#/components/schemas/BundleRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/Bundle"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"servers":[{"description":"Production","url":"https://api.ironfang.com/render"}],"summary":"Capture a bundle from one navigation","tags":["Render"]}},"/v1/capabilities":{"get":{"description":"A dated statement of capabilities with a status of `live`, `planned`\nor `not_offered`. Public and cacheable; the comparison pages are\nchecked against the same file, so nothing is advertised before it is\nlive.\n","operationId":"getCapabilities","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"capabilities":{"items":{"properties":{"key":{"type":"string"},"name":{"type":"string"},"since":{"format":"date","type":"string"},"status":{"enum":["live","planned","not_offered"],"type":"string"}},"type":"object"},"type":"array"},"product":{"type":"string"},"updated":{"format":"date","type":"string"}},"type":"object"}}},"description":"The capability catalogue."}},"security":[],"summary":"What Ironfang Render does, is building and does not offer","tags":["Account"]}},"/v1/deliveries":{"get":{"description":"Newest first, optionally for one `destination`. A delivery is\n`pending`, `delivering`, `delivered` or `failed`; `failed` means it\nused up its attempts. Delivery state never affects the job's own\noutcome. Filters apply before the cursor boundary. `pending` selects\npending deliveries with no attempts; `retrying` selects pending deliveries\nwith at least one attempt. `job_id` selects one owned job.\n","operationId":"listDeliveries","parameters":[{"in":"query","name":"destination","schema":{"format":"uuid","type":"string"}},{"in":"query","name":"job_id","schema":{"format":"uuid","type":"string"}},{"in":"query","name":"status","schema":{"enum":["pending","retrying","delivering","delivered","failed"],"type":"string"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":100,"minimum":1,"type":"integer"}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryList"}}},"description":"Deliveries and next_cursor, with no-store caching."},"400":{"description":"Invalid, repeated or unknown query parameter."}},"summary":"List deliveries","tags":["Destinations"]}},"/v1/deliveries/{id}":{"get":{"description":"The list omits payloads because they are the largest part of a\ndelivery and are wanted one at a time, when something has gone wrong.\nThis returns the body exactly as it went on the wire, plus\n`payload_timestamp` and `payload_signature` - the two values the\nsignature is computed over - so you can reproduce the HMAC check\nagainst your own secret and see where it diverges.\n\n`response_body` is what your endpoint sent back, bounded to 4 KB.\n\nBodies are kept for `payload_retention_days` (7) after the attempt\nand then blanked; the delivery row itself stays, so the history of\nwhat was sent and how it went is not lost with it.\n","operationId":"getDelivery","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryRecord"}}},"description":"The delivery. Payload is absent once purged."},"404":{"description":"No such delivery on this account."}},"summary":"One delivery, with the body it posted","tags":["Destinations"]}},"/v1/deliveries/{id}/redeliver":{"post":{"description":"Puts a delivery back on the queue with a fresh attempt ladder, for\nafter you have fixed whatever was wrong. The webhook body is rebuilt\nat send time, so the result link in it is good for 15 minutes from\nthen rather than from the original attempt.\n","operationId":"redeliverDelivery","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryRecord"}}},"description":"The delivery, queued again."},"404":{"description":"No such delivery on this account."}},"summary":"Send a delivery again","tags":["Destinations"]}},"/v1/destinations":{"get":{"description":"Live destinations, newest first. Secrets and credentials are never included.","operationId":"listDestinations","responses":{"200":{"description":"The account's destinations."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key carries no `render:destinations` scope, or is not account-backed."}},"summary":"List destinations","tags":["Destinations"]},"post":{"description":"A destination is created once and named by id from every job, so a\nsigning secret or a set of S3 keys is sent to us on one request and\nnever appears in a job body again.\n\nA `webhook` destination needs a public `https` URL. The response\ncarries `signing_secret` **once**: we keep only an encrypted copy,\nso there is no endpoint that can show it to you again.\n\n`signing_secret` is 64 hex characters: the hex encoding of a 32-byte\nkey. Each delivery carries `Ironfang-Signature: v1=\u003chex\u003e`, the\nHMAC-SHA256 of `\u003cIronfang-Timestamp\u003e.\u003craw body\u003e` keyed with those 32\nbytes - decode the secret from hex before verifying, rather than\nusing its text. The SDKs' `verifyWebhook` / `verify_webhook` take\nthe secret as shown and decode it (from 3.0.1).\n\nAn `s3` destination needs `bucket`, `region`, `access_key` and\n`secret_key`; `endpoint` (for a non-AWS provider), `path_style` and\n`prefix` are optional. Scope the credentials to the prefix you give\nus. They are never returned after creation.\n\nNeeds the `render:destinations` scope, which maps to the\n`destinations.manage` permission.\n","operationId":"createDestination","requestBody":{"content":{"application/json":{"schema":{"properties":{"access_key":{"type":"string"},"bucket":{"type":"string"},"endpoint":{"format":"uri","type":"string"},"name":{"maxLength":120,"type":"string"},"path_style":{"type":"boolean"},"prefix":{"maxLength":256,"type":"string"},"region":{"type":"string"},"secret_key":{"type":"string"},"session_token":{"type":"string"},"type":{"enum":["webhook","s3"],"type":"string"},"url":{"description":"webhook: a public https URL.","format":"uri","type":"string"}},"required":["type"],"type":"object"}}},"required":true},"responses":{"201":{"description":"The destination. A webhook also carries `signing_secret`, shown once: 64 hex characters encoding the 32-byte HMAC key. Decode hex before verifying signatures."},"400":{"description":"`bad_destination`: not a public https URL, or missing bucket credentials."},"403":{"description":"The key carries no `render:destinations` scope."},"501":{"description":"This deployment has no credential vault configured."}},"summary":"Register a delivery destination","tags":["Destinations"]}},"/v1/destinations/{id}":{"delete":{"description":"It stops receiving anything and leaves the list. Its past deliveries\nstay, so history still reads true.\n","operationId":"deleteDestination","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Removed."},"404":{"description":"No such destination on this account."}},"summary":"Remove a destination","tags":["Destinations"]},"get":{"description":"Returns a delivery destination belonging to the authenticated account. Stored delivery credentials are not included in the response.","operationId":"getDestination","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The destination."},"404":{"description":"No such destination on this account."}},"summary":"Get a destination","tags":["Destinations"]},"patch":{"description":"`name` and `enabled` only. An address or a credential is not edited in\nplace - create a new destination and point your jobs at it, so a\ndelivery's history always says where it actually went. Re-enabling\nclears the failure count.\n","operationId":"updateDestination","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"enabled":{"type":"boolean"},"name":{"maxLength":120,"type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"description":"The destination."},"404":{"description":"No such destination on this account."}},"summary":"Rename or enable a destination","tags":["Destinations"]}},"/v1/destinations/{id}/test":{"post":{"description":"A webhook receives a signed `render.destination.test` event; a\nbucket has a uniquely named object written to it. Always `200`: the\nbody's `ok` says whether the destination answered, and `error` says\nwhat went wrong if it did not.\n","operationId":"testDestination","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`ok` and, on failure, `error`."},"404":{"description":"No such destination on this account."}},"summary":"Test a destination now","tags":["Destinations"]}},"/v1/document-templates":{"get":{"description":"The account's document templates, newest first, without their sources. Archived ones only with `archived=true`.","operationId":"listDocumentTemplates","parameters":[{"in":"query","name":"archived","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"templates":{"items":{"$ref":"#/components/schemas/DocumentTemplate"},"type":"array"}},"type":"object"}}},"description":"The templates."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List document templates","tags":["Document templates"]},"post":{"description":"Stores a draft. It renders only once published, or as a preview. At most 500 templates per account, archived ones included.","operationId":"createDocumentTemplate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentTemplateInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentTemplate"}}},"description":"Created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"`too_many_templates`."},"422":{"$ref":"#/components/responses/DocumentFailed"}},"summary":"Create a document template","tags":["Document templates"]}},"/v1/document-templates/{id}":{"delete":{"description":"Archives the template: it can no longer be edited, published or rendered\nwithout a revision number. Its revisions are kept, so jobs accepted\nbefore still render, and a named revision still renders.\n","operationId":"archiveDocumentTemplate","responses":{"204":{"description":"Archived."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Archive a document template","tags":["Document templates"]},"get":{"description":"The template and its draft.","operationId":"getDocumentTemplate","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentTemplate"}}},"description":"The template."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Fetch a document template","tags":["Document templates"]},"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"}],"put":{"description":"Replaces the draft. Published revisions and what is published are unchanged until the next publish.","operationId":"updateDocumentTemplate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentTemplateInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentTemplate"}}},"description":"Updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`template_archived`."},"422":{"$ref":"#/components/responses/DocumentFailed"}},"summary":"Replace a document template's draft","tags":["Document templates"]}},"/v1/document-templates/{id}/preview":{"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"}],"post":{"description":"Prints the draft with the body's data, else the sample data: what\npublishing it would produce. A PDF render on the `render.pdf` meter,\nlike `POST /pdf`. It answers a PDF only when the render succeeded; any\nfailure is a JSON error.\n","operationId":"previewDocumentTemplate","requestBody":{"content":{"application/json":{"schema":{"properties":{"data":{"type":"object"}},"type":"object"}}}},"responses":{"200":{"$ref":"#/components/responses/DocumentPdf"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/DocumentFailed"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"Preview the draft as a PDF","tags":["Document templates"]}},"/v1/document-templates/{id}/publish":{"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"}],"post":{"description":"Makes the draft the published revision: a new immutable revision,\nnumbered after the last - or, when the draft is exactly the latest\nrevision, that one again. Jobs already accepted keep their revision.\n","operationId":"publishDocumentTemplate","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRevision"}}},"description":"The published revision."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`template_archived`."},"422":{"$ref":"#/components/responses/DocumentFailed"}},"summary":"Publish the draft","tags":["Document templates"]}},"/v1/document-templates/{id}/render":{"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"}],"post":{"description":"Prints a revision - the published one unless `revision` names another -\nwith `data`. The data is checked and the template run first, before\nanything is reserved: problems are a 422 that costs nothing. Then it\nprints like `POST /pdf`, on the `render.pdf` meter. PDFs are not cached.\nAs a job, submit kind `document`.\n","operationId":"renderDocumentTemplate","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRenderRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/DocumentPdf"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`template_unpublished`, `template_archived` (name a revision), or `idempotency_conflict`."},"422":{"$ref":"#/components/responses/DocumentFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Render a document","tags":["Document templates"]}},"/v1/document-templates/{id}/revisions":{"get":{"description":"The template's revisions, newest first, without their sources.","operationId":"listDocumentRevisions","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"revisions":{"items":{"$ref":"#/components/schemas/DocumentRevision"},"type":"array"}},"type":"object"}}},"description":"The revisions."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List revisions","tags":["Document templates"]},"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"}]},"/v1/document-templates/{id}/revisions/{n}":{"get":{"description":"One revision, with its source.","operationId":"getDocumentRevision","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRevision"}}},"description":"The revision."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Fetch a revision","tags":["Document templates"]},"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"},{"$ref":"#/components/parameters/RevisionNumber"}]},"/v1/document-templates/{id}/revisions/{n}/publish":{"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"},{"$ref":"#/components/parameters/RevisionNumber"}],"post":{"description":"Points unversioned renders at this revision - a rollback, or forward again. Nothing else changes.","operationId":"publishDocumentRevision","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRevision"}}},"description":"The published revision."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`template_archived`."}},"summary":"Publish an existing revision","tags":["Document templates"]}},"/v1/document-templates/{id}/validate":{"parameters":[{"$ref":"#/components/parameters/DocumentTemplateId"}],"post":{"description":"Compiles the draft, checks the data (the body's, else the sample data)\nagainst its schema and runs the template, without printing or billing.\nAlways 200; `valid` says whether it would render.\n","operationId":"validateDocumentTemplate","requestBody":{"content":{"application/json":{"schema":{"properties":{"data":{"type":"object"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"error":{"description":"A compile or run error.","type":"string"},"fields":{"description":"Data problems by path.","items":{"type":"object"},"type":"array"},"valid":{"type":"boolean"}},"type":"object"}}},"description":"The answer."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Validate a draft against data","tags":["Document templates"]}},"/v1/image/{id}":{"parameters":[{"$ref":"#/components/parameters/TemplateId"}],"post":{"description":"Substitutes `vars` into the template's `{{placeholders}}` and renders it\nat the template's own width and height. Returns the image bytes.\n","operationId":"renderTemplate","requestBody":{"content":{"application/json":{"examples":{"card":{"value":{"format":"png","vars":{"title":"Shipping UUIDv7 everywhere"}}}},"schema":{"$ref":"#/components/schemas/RenderTemplateRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/Image"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Render a template","tags":["Templates"]}},"/v1/jobs":{"get":{"description":"Newest first. `status` filters; `active` includes queued, running and cancellation_requested. `limit` up to 100; pass `next_cursor` back to continue.","operationId":"listJobs","parameters":[{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"maximum":100,"type":"integer"}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"Jobs and `next_cursor`."},"400":{"description":"`bad_request`: `cursor` is not one this endpoint issued."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key carries no `render:render` scope, or is not account-backed."}},"summary":"List jobs","tags":["Jobs"]},"post":{"description":"The same renders as the synchronous endpoints - `kind` is one of\n`screenshot`, `pdf`, `qr`, `image`, `clip`, `site_preview`, `document`\nor `bundle` and\n`request` is that endpoint's body (an `image` job names its\n`template` inside `request`) - accepted with `202` and run by a\nworker. Poll the job, or cancel it, then collect the result while it\nis hosted: 24 hours, private, through a signed link valid for 15\nminutes. A process restart after the `202` never loses the job.\n\nSend `Idempotency-Key` on every submission. The same key with the same\nrequest returns the existing job with `200`; the same key with a\ndifferent request is a `409 idempotency_conflict`. `external_id` is\nyour own reference, up to 120 characters.\n\nBilling: the most the job can count is reserved on your billing\naccount when it is accepted - in the same step, so a job you were told\nabout always holds its reservation, and a refusal leaves no job. The\none reservation is kept across retries of the job and committed, with\nwhat the job produced, when it succeeds; a site preview reserves the\n60 second limit and commits its real length. A job that fails,\nis cancelled while queued, or is cancelled while running without a\nusable output, counts nothing. The job's `usage` says what it holds:\n`{\"meter\", \"reserved\", \"committed\"}`.\n\nCapacity: an account may have 1,000 unfinished jobs (queued, running\nor being cancelled); a job past that is `429 queue_full` and is not\ncreated. A job that uses the browser runs in one of the account's 2\nbrowser slots, shared with its synchronous renders; while both are\nbusy it waits in the queue rather than being refused.\n","operationId":"submitJob","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"delivery":{"$ref":"#/components/schemas/JobDelivery"},"external_id":{"maxLength":120,"type":"string"},"kind":{"enum":["screenshot","pdf","qr","image","clip","site_preview","document","bundle"],"type":"string"},"request":{"description":"The synchronous endpoint's request body for that kind. For\n`document`: `{\"template\": \"\u003cid\u003e\", \"revision\": \u003cnumber\u003e,\n\"data\": {...}}`, the revision optional. The revision is\nresolved when the job is accepted - the published one unless\nnamed - and the job renders it on every attempt, however the\ntemplate changes meanwhile. The data is checked against that\nrevision's schema then: problems are a 422 `data_invalid`\nwith `fields`.\n","type":"object"}},"required":["kind","request"],"type":"object"}}},"required":true},"responses":{"200":{"description":"The same Idempotency-Key and request were seen before; this is that job."},"202":{"description":"The job was accepted; the body is the job resource."},"400":{"description":"Invalid submission."},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"description":"The key carries no `render:render` scope, or is not account-backed."},"404":{"description":"The template named by an image job does not exist."},"409":{"description":"`idempotency_conflict`: the key was used with a different request."},"429":{"$ref":"#/components/responses/RateLimited"},"501":{"description":"This deployment cannot render clips or site previews."},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Submit a durable render job","tags":["Jobs"]}},"/v1/jobs/{id}":{"delete":{"description":"A queued job is cancelled and its reservation released at once; a running one stops at its next safe point, and counts only if it had already produced a usable output.","operationId":"cancelJob","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The job, cancelled or with cancellation requested."},"404":{"description":"No such job on this account."},"409":{"description":"`job_finished`: nothing left to cancel."}},"summary":"Cancel a job","tags":["Jobs"]},"get":{"description":"`status` is `queued`, `running`, `cancellation_requested`,\n`succeeded`, `failed` or `cancelled`. A succeeded job carries\n`result` with `content_type`, `bytes`, `sha256`, `expires_at` and,\nwhile hosted, a signed `url` valid for 15 minutes - an absolute link\nthat needs no API key. A failed job\ncarries `error.code` and `error.message`. `usage` names the meter\nand the quantity reserved and committed. The submitted request is\nnever returned; `request_summary` is the redacted shape.\n\nA screenshot, PDF or image job carries `diagnostics`\n([Diagnostics](#/components/schemas/Diagnostics)) once an attempt has\nloaded the page, failed attempts included: a succeeded job with\n`diagnostics.target_status: 404` captured a not-found page. The\nwebhook's `data` carries the same block.\n","operationId":"getJob","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The job resource."},"404":{"description":"No such job on this account."}},"summary":"Poll a job","tags":["Jobs"]}},"/v1/jobs/{id}/result":{"get":{"description":"Redirects (`302`) to a signed download link valid for 15 minutes; the\nlink needs no API key. With `redirect=false` the file is returned\ndirectly instead, for clients that do not follow redirects. The\nhosted result is kept for 24 hours after success, privately, on\nIronfang's side - not permanent hosting.\n","operationId":"getJobResult","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"`false` returns the file itself rather than a redirect to it.","in":"query","name":"redirect","required":false,"schema":{"default":true,"type":"boolean"}}],"responses":{"200":{"description":"With `redirect=false`, the result bytes with their recorded content type."},"302":{"description":"Redirect to the signed download link."},"404":{"description":"No such job on this account."},"409":{"description":"`result_not_ready`: the job has not succeeded."},"410":{"description":"`result_expired`: the hosted result is gone; run the job again."}},"summary":"Collect a job's result","tags":["Jobs"]}},"/v1/pdf":{"post":{"description":"Supply exactly one of `url` or `html`. Returns `application/pdf`.\n\n`header_html` and `footer_html` are Chromium print templates: they\nsupport the classes `date`, `title`, `url`, `pageNumber` and\n`totalPages`, which Chromium substitutes at print time.\n\nCounts one `render.pdf`. A PDF may have at most 100 pages; a document\nthat prints to more is refused with `422 pdf_too_long` and nothing is\ncounted. PDFs are never cached.\n","operationId":"createPdf","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"examples":{"invoice":{"summary":"An invoice with a page footer","value":{"footer_html":"\u003cdiv style=\"font-size:9px;margin:0 auto\"\u003e\u003cspan class=\"pageNumber\"\u003e\u003c/span\u003e of \u003cspan class=\"totalPages\"\u003e\u003c/span\u003e\u003c/div\u003e","html":"\u003ch1\u003eInvoice 1024\u003c/h1\u003e","print_background":true}},"page":{"summary":"A page, landscape","value":{"landscape":true,"print_background":true,"url":"https://example.com/report"}}},"schema":{"$ref":"#/components/schemas/PdfRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/Pdf"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Render a PDF","tags":["Render"]}},"/v1/qr":{"post":{"description":"Styled, optionally logo-bearing QR codes, drawn natively and\n**verified before delivery**: every response is decoded and compared\nto `data` on the way out. A code that would not scan is a\n`422 unscannable` and nothing is counted - the guarantee is the\nfeature.\n\nA logo forces the highest error-correction level and is composited\nover the center on a knockout tile (`logo_pad`). Send it inline as a\n`data:` URI (`logo`) or by URL (`logo_url`); remote logos face the\nsame target guard and rate caps as screenshot URLs.\n\nQRs are deterministic, so identical requests hit the cache and cache\nhits count nothing. Each code counts one `render.qr`, a free utility:\nit shows in usage and is never charged. Output never carries a\nbadge - a mark inside a scannable symbol would corrupt it.\n\nTo place a QR inside a stored template render, use the `qr` object\non `POST /v1/image/{id}` instead - each entry becomes a\n`{{qr.\u003cname\u003e}}` variable holding a data URI.\n","operationId":"createQr","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"examples":{"branded":{"summary":"Rounded style with a centered logo","value":{"dark":"#1b2a4a","data":"https://example.com","dots":"circle","logo_size":0.22,"logo_url":"https://example.com/logo.png","size":600}},"plain":{"summary":"A link, defaults throughout","value":{"data":"https://example.com/menu"}}},"schema":{"$ref":"#/components/schemas/QrRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/Image"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Render a QR code","tags":["Render"]}},"/v1/r/{exp}/{sig}":{"get":{"description":"The endpoint a signed URL points at. Takes no API key - the signature is\nthe credential - so it can be embedded directly in HTML or email. Call\n`POST /v1/sign` to mint one rather than constructing it by hand.\n","operationId":"renderSignedUrl","parameters":[{"description":"Expiry, in unix seconds, part of the signed payload. `0` never\nexpires.\n","in":"path","name":"exp","required":true,"schema":{"type":"string"}},{"description":"The signature.","in":"path","name":"sig","required":true,"schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Image"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Signature invalid or expired."},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Signing is not configured on this deployment."},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"security":[],"summary":"Render from a signed URL","tags":["Signed URLs"]}},"/v1/requests":{"get":{"description":"One row per authenticated render in the last seven days, newest\nfirst, synchronous calls and jobs alike: kind, outcome, safe error\ncode, cache hit or miss, the `meter` it counted against and the\n`quantity` it committed (0 for a cache hit or a failure), response\nsize and type, duration, the target's origin and path, the names of\nthe option fields supplied, the request id you can quote to support\nand, for a render that loaded a page, its `diagnostics`\n([Diagnostics](#/components/schemas/Diagnostics)). A cache hit\nrendered nothing and has none. A job cancelled before it ran, or\nfailed because no worker could finish it, has its row too.\n\nIt is diagnostic, not replay. Nothing here can rebuild the request:\nheader and cookie values, HTML, template variables, URL query strings\nand signatures are never recorded. Retention is seven days for every\naccount.\n\n`id` matches the request id, a job id or your own `X-Request-ID`,\nwhich is how you arrive here from an error message.\n","operationId":"listRequests","parameters":[{"in":"query","name":"since","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"until","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"outcome","schema":{"enum":["ok","error","cancelled"],"type":"string"}},{"in":"query","name":"kind","schema":{"enum":["screenshot","pdf","qr","image","clip","site_preview","document","bundle"],"type":"string"}},{"in":"query","name":"cache","schema":{"enum":["hit","miss"],"type":"string"}},{"description":"API key id","in":"query","name":"key","schema":{"type":"string"}},{"description":"Safe error code","in":"query","name":"error","schema":{"type":"string"}},{"description":"Request id, job id or your X-Request-ID","in":"query","name":"id","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"type":"integer"}},{"in":"query","name":"offset","schema":{"default":0,"type":"integer"}}],"responses":{"200":{"description":"`requests` and `retention_days`."},"400":{"$ref":"#/components/responses/BadRequest"}},"summary":"Request history","tags":["Account"]}},"/v1/results/{exp}/{sig}":{"get":{"description":"The link a job's `result.url` points at. The signature is the authorisation; it expires after 15 minutes.","operationId":"getSignedResult","parameters":[{"in":"path","name":"exp","required":true,"schema":{"type":"string"}},{"in":"path","name":"sig","required":true,"schema":{"type":"string"}},{"in":"query","name":"job","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The result bytes with their recorded content type."},"403":{"description":"Bad or expired signature."},"410":{"description":"The hosted result has expired."}},"security":[],"summary":"Signed result download","tags":["Jobs"]}},"/v1/screenshot":{"post":{"description":"Supply exactly one of `url` or `html`. Returns the image bytes.\nCounts one `render.screenshot`.\n","operationId":"createScreenshot","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"examples":{"element":{"summary":"One element, on a dark-mode page","value":{"dark_mode":true,"selector":".pricing-table","url":"https://example.com/pricing"}},"html":{"summary":"Your own markup, as JPEG","value":{"format":"jpeg","height":630,"html":"\u003ch1 style=\"font:600 48px sans-serif\"\u003eHello\u003c/h1\u003e","quality":85,"width":1200}},"url":{"summary":"A page, full length","value":{"full_page":true,"url":"https://example.com","width":1280}}},"schema":{"$ref":"#/components/schemas/ScreenshotRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/Image"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Render a screenshot","tags":["Render"]}},"/v1/sign":{"post":{"description":"Returns a URL that renders on GET without an API key - safe to put in an\n`\u003cimg\u003e` tag or an email. The URL is bound to your account, so renders\nthrough it are admitted and counted against your organisation's\nbilling account like any other.\n\nA signed URL expires after 30 days unless `ttl_hours` says otherwise:\nany number of hours, or `0` for a URL that never expires, which is\nwhat an `og:image` tag needs. The response says when the URL\nexpires.\n\nRequires signing to be configured on the deployment; returns `501`\notherwise.\n","operationId":"createSignedUrl","requestBody":{"content":{"application/json":{"examples":{"screenshot":{"value":{"kind":"screenshot","ttl_hours":24,"url":"https://example.com","width":1280}},"template":{"value":{"kind":"image","template":"0193f0c4-1f4e-7a91-9c3e-2b6f5d8a1e77","vars":{"title":"Hello"}}}},"schema":{"$ref":"#/components/schemas/SignRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedUrl"}}},"description":"The signed URL."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"501":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Signing is not configured on this deployment."}},"summary":"Mint a signed render URL","tags":["Signed URLs"]}},"/v1/site-preview":{"post":{"description":"Loads and settles a website, preloads lazy content, then records a\ntop-to-bottom browser motion at 30 frames per second. `per_page` moves\none viewport at a time and pauses for 750ms between steps.\n`single_sweep` makes one continuous eased pass. Sticky and fixed\nelements behave as they do in the browser.\n\nThe response is one `multipart/form-data` payload containing the first\nvideo frame as `poster.jpg` and the H.264 video as `preview.mp4`.\nIronfang Render calculates the duration from the page height and selected\nmotion, up to a 60 second hard limit. A preview counts its output\nseconds, rounded up, against `render.site_preview_second`: the 60\nsecond limit is reserved before the recording starts and the real\nlength is committed when the preview is delivered. Failed work counts\nnothing and cached repeats are free.\n\nThis endpoint is available on the product-partitioned Ironfang Render URL\nonly. It has no unprefixed `/v1/site-preview` compatibility route.\n","operationId":"createSitePreview","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"examples":{"showcase":{"summary":"A showcase card preview","value":{"height":494,"motion":"per_page","url":"https://example.com","width":672}}},"schema":{"$ref":"#/components/schemas/SitePreviewRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/SitePreview"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"501":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"This deployment does not have browser video capture configured."},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"servers":[{"description":"Production","url":"https://api.ironfang.com/render"}],"summary":"Render a scrolling website preview","tags":["Render"]}},"/v1/templates":{"get":{"description":"Paged newest first. Follow next_cursor until empty; `total` is the whole count and does not change between pages. summary=true returns an empty html field; fetch a template by ID to edit or render it.","operationId":"listTemplates","parameters":[{"description":"Opaque next_cursor from the previous page; empty means the first page.","in":"query","name":"cursor","schema":{"maxLength":256,"type":"string"}},{"description":"Maximum templates per page.","in":"query","name":"limit","schema":{"default":50,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Omit HTML bodies from the database read.","in":"query","name":"summary","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"example":{"next_cursor":"","templates":[{"height":630,"id":"01a01c7c-682e-7b2a-95cd-ffb2f2a4d9be","name":"og-card","width":1200}],"total":1},"schema":{"$ref":"#/components/schemas/TemplateList"}}},"description":"Your templates, wrapped in a `templates` property - not a bare\narray. Clients that iterate must read `templates`.\n"},"400":{"description":"Malformed cursor, repeated or unknown parameter, or limit outside 1-100."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List templates","tags":["Templates"]},"post":{"description":"Store HTML containing `{{placeholders}}`. Render it with\n`POST /v1/image/{id}`, supplying values for the placeholders.\n","operationId":"createTemplate","requestBody":{"content":{"application/json":{"examples":{"og":{"summary":"An Open Graph card","value":{"height":630,"html":"\u003cdiv style=\"font:700 64px sans-serif;padding:80px\"\u003e{{title}}\u003c/div\u003e","name":"og-card","width":1200}}},"schema":{"$ref":"#/components/schemas/TemplateInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}},"description":"Created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Create a template","tags":["Templates"]}},"/v1/templates/{id}":{"delete":{"description":"Removes a stored template from the authenticated account so it can no longer be used for new template renders.","operationId":"deleteTemplate","responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a template","tags":["Templates"]},"get":{"description":"Returns a stored rendering template belonging to the authenticated account, including its HTML and output dimensions.","operationId":"getTemplate","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}},"description":"The template."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Fetch a template","tags":["Templates"]},"parameters":[{"$ref":"#/components/parameters/TemplateId"}],"put":{"description":"Replaces the authenticated account's stored template with the supplied name, HTML and dimensions. Subsequent renders use the updated template.","operationId":"updateTemplate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}},"description":"Updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Replace a template","tags":["Templates"]}},"/v1/usage":{"get":{"description":"This UTC calendar month's Ironfang Render usage from your\norganisation's billing account: for every Render meter, what was used,\nhow much of it the free allowance covered, what is held by work in\nflight, and what the settled usage costs before tax. The whole\naccount - every product, budgets, statements - is `/billing/v1`.\n\nAn env bootstrap key, or a deployment with billing off, is unmetered\nand answers `{\"unmetered\": true}`.\n","operationId":"getUsage","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"}}},"description":"Current period usage."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"summary":"Usage this period","tags":["Account"]}},"/v1/video":{"post":{"description":"A background, captions that appear on a schedule, an optional\nwatermark and an optional audio bed, encoded to MP4.\n\nRendered inside the request, so clips are capped at 60 seconds. A\nclip counts its output seconds, rounded up: `render.video_second_fhd`\nfor the 1080p sizes (`vertical`, `landscape`) and\n`render.video_second_hd` for `720p` and `square`. Its duration is\nreserved before the encode and committed when the clip is delivered;\na clip that fails to render counts nothing.\n\nEvery asset URL is fetched by us through the same guard a screenshot\ntarget faces, and each is capped at 64 MB.\n","operationId":"createClip","parameters":[{"$ref":"#/components/parameters/SyncIdempotencyKey"}],"requestBody":{"content":{"application/json":{"examples":{"captioned":{"summary":"A captioned vertical clip on a solid colour","value":{"captions":[{"from":0,"text":"Ship it on Friday","to":5},{"from":5,"text":"Find out on Monday","to":10},{"from":10,"text":"Or gate the deploy","to":15}],"colour":"#101820","duration":15,"size":"vertical"}},"overBackground":{"summary":"Over an image, with a logo and music","value":{"audio":"https://example.com/bed.m4a","background":"https://example.com/backdrop.jpg","captions":[{"from":1,"text":"New in August","to":6}],"duration":20,"size":"square","watermark":"https://example.com/logo.png"}}},"schema":{"$ref":"#/components/schemas/ClipRequest"}}},"required":true},"responses":{"200":{"$ref":"#/components/responses/Clip"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/BillingRefused"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/RenderFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"501":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"This deployment has no encoder configured."},"503":{"$ref":"#/components/responses/BillingUnavailable"}},"summary":"Render a clip","tags":["Render"]}}},"security":[{"apiKey":[]}],"servers":[{"description":"Production","url":"https://api.ironfang.com/render"},{"description":"Production, unpartitioned - retained for existing clients","url":"https://api.ironfang.com"}],"tags":[{"description":"Turn a URL or HTML into an image, PDF or video.","name":"Render"},{"description":"Stored HTML with `{{variable}}` placeholders, rendered on demand.","name":"Templates"},{"description":"Stored PDF templates with structured JSON data: nested values, loops and\nconditionals, a declared data schema checked before rendering, an\neditable draft and immutable numbered revisions. Unversioned renders and\njobs use the published revision, resolved when they are accepted.\n","name":"Document templates"},{"description":"Shareable render URLs that need no API key.","name":"Signed URLs"},{"description":"Up to 100 renders submitted, and refused, together.","name":"Batches"},{"description":"Where a finished job goes - a signed webhook, or your own S3 bucket.","name":"Destinations"},{"description":"Asynchronous jobs - status, result, cancellation.","name":"Jobs"},{"description":"Usage this period, from the billing account.","name":"Account"}]}