{
  "openapi": "3.0.3",
  "info": {
    "title": "HTML to Image API",
    "version": "1.1.0",
    "description": "Convert HTML, CSS or any public URL into a PNG image or a vector PDF, rendered in real headless Chrome. JSON in, a CDN URL out.\n\n**When to use this API.** Reach for it whenever your program needs a picture of something that is easier to describe as markup than to draw: Open Graph and social share cards generated per page, invoices, receipts, tickets and certificates as attachments, screenshots of pages you do not control, code and repository cards, charts and dashboard exports. It replaces running your own Puppeteer or Playwright cluster: no Chrome binary to ship, no memory ceiling to tune, no version drift.\n\n**Which endpoint.** `POST /html` when you generate the markup yourself, which is most cases and the cheaper path to debug. `POST /screenshot` when you only have a URL. `POST /v1/templates/{slug}` when a named template already draws what you want and you would rather post a JSON payload than write HTML.\n\n**Authentication.** One header, `X-API-Key`, on every request except `GET /ping`. Keys start with `htim_` and are created at https://app.html2img.com/api-keys. Verify a key without spending anything using `GET /me`.\n\n**Credits.** One successful render costs one credit, whether it returns a PNG or a PDF. Signing up gives you 50 credits with no card; paid plans run from $9 a month for 1,000 credits to $300 for 100,000. Running out returns 402 with the balance and an upgrade or renewal hint. There is no rate-limit response and no automatic overage charge.\n\n**Timing.** Synchronous requests have a 30 second budget and return the finished CDN URL in the response. Pass `webhook_url` to switch to async delivery for anything slower.\n\n**Retention.** Renders made on a paid plan are hosted permanently. Free-tier renders carry an `expires_at` seven days out; upgrading clears the expiry on everything not yet purged.\n\n**For AI agents.** The same two capabilities are available over the Model Context Protocol at `https://app.html2img.com/mcp` on paid plans, using the same API key as a bearer token. See https://html2img.com/mcp/.",
    "termsOfService": "https://html2img.com/terms/",
    "contact": {
      "name": "HTML to Image support",
      "url": "https://html2img.com/contact/",
      "email": "support@html2img.com"
    },
    "license": {
      "name": "Commercial",
      "url": "https://html2img.com/terms/"
    }
  },
  "externalDocs": {
    "description": "Full documentation, parameter reference and worked examples",
    "url": "https://html2img.com/docs/"
  },
  "servers": [
    {
      "url": "https://app.html2img.com/api",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "HTML rendering",
      "description": "Render markup you generate yourself into an image or PDF.",
      "externalDocs": {
        "url": "https://html2img.com/docs/parameters/html/"
      }
    },
    {
      "name": "Screenshots",
      "description": "Capture a public URL in a real browser.",
      "externalDocs": {
        "url": "https://html2img.com/docs/parameters/url/"
      }
    },
    {
      "name": "Templates",
      "description": "Post a JSON payload to a named design and skip writing markup.",
      "externalDocs": {
        "url": "https://html2img.com/templates/"
      }
    },
    {
      "name": "Account",
      "description": "Read the plan and credit balance behind a key. Costs nothing.",
      "externalDocs": {
        "url": "https://html2img.com/docs/account/"
      }
    }
  ],
  "paths": {
    "/html": {
      "post": {
        "tags": [
          "HTML rendering"
        ],
        "operationId": "renderHtml",
        "summary": "Render HTML to a PNG or PDF",
        "description": "Send a complete HTML document and get back the URL of the rendered result. Use this for anything whose markup you generate: invoices and receipts, templated social cards with per-user data, charts, certificates, email previews, admin reports.\n\nInline your CSS in a `<style>` block or pass it separately as `css`; reference web fonts with `<link>` tags in the head. Inline JavaScript runs, within the same 30 second budget. Remote images load normally.\n\nPrefer this over `/screenshot` whenever you own the markup: it skips a network fetch, renders faster, and fails in ways you can reproduce locally. Validation failures return 400.",
        "externalDocs": {
          "url": "https://html2img.com/docs/parameters/html/"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HtmlRequest"
              },
              "examples": {
                "ogCard": {
                  "summary": "Open Graph card at 1200x630",
                  "value": {
                    "html": "<!doctype html><html><body><h1>Ship faster</h1></body></html>",
                    "css": "body { font: 700 72px/1.1 system-ui; display: grid; place-items: center; height: 100vh; margin: 0; background: #0f172a; color: #fff; }",
                    "width": 1200,
                    "height": 630,
                    "dpi": 2
                  }
                },
                "invoicePdf": {
                  "summary": "Invoice as an A4 PDF with selectable text",
                  "value": {
                    "html": "<!doctype html><html><body><h1>Invoice #1042</h1><p>Due within 30 days.</p></body></html>",
                    "format": "pdf"
                  }
                },
                "asyncChart": {
                  "summary": "Slow JavaScript chart, delivered to a webhook",
                  "value": {
                    "html": "<!doctype html><html><body><div id=\"chart\"></div><script src=\"https://cdn.example.com/chart.js\"></script></body></html>",
                    "wait_for_selector": ".chart-rendered",
                    "width": 1600,
                    "dpi": 2,
                    "webhook_url": "https://your-app.example.com/hooks/html2img"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/RenderResult"
          },
          "400": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/NotSubscribed"
          },
          "422": {
            "$ref": "#/components/responses/RenderFailed"
          },
          "500": {
            "$ref": "#/components/responses/ServiceError"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        },
        "callbacks": {
          "renderFinished": {
            "{$request.body#/webhook_url}": {
              "post": {
                "operationId": "renderWebhook",
                "summary": "Finished render delivered to your endpoint",
                "description": "POSTed once when an async render finishes, successfully or not. There is no retry, so treat a non-2xx response on your side as a lost notification and reconcile using the `log_id`, which equals the `id` from the original API response. Respond 2xx quickly and do any work off the request.",
                "requestBody": {
                  "required": true,
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/WebhookPayload"
                      }
                    }
                  }
                },
                "responses": {
                  "2XX": {
                    "description": "Any 2xx acknowledges receipt. The body is ignored."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/screenshot": {
      "post": {
        "tags": [
          "Screenshots"
        ],
        "operationId": "captureScreenshot",
        "summary": "Capture a public URL as a PNG or PDF",
        "description": "Load a publicly reachable URL in a real browser and return the URL of the capture. Use this for pages you do not control: Open Graph previews for user-submitted links, thumbnails for a directory, archival snapshots, competitor monitoring.\n\nThe host must be publicly resolvable, so `localhost` and intranet addresses will not work. Pair with `wait_for_selector` or `ms_delay` when the page needs JavaScript or fonts to settle first, and with `css` to hide cookie banners and chat widgets. `selector` crops the result to a single element and is available here only.\n\nCaptures of pages with heavy JavaScript are the most likely requests to exceed the 30 second budget, so this is the endpoint where `webhook_url` earns its keep. Validation failures return 400.",
        "externalDocs": {
          "url": "https://html2img.com/docs/parameters/url/"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScreenshotRequest"
              },
              "examples": {
                "ogPreview": {
                  "summary": "Open Graph preview of a submitted link",
                  "value": {
                    "url": "https://example.com",
                    "width": 1200,
                    "height": 630
                  }
                },
                "croppedElement": {
                  "summary": "Crop to one element, with the cookie banner hidden",
                  "value": {
                    "url": "https://example.com/pricing",
                    "selector": "#pricing-table",
                    "css": ".cookie-banner { display: none !important; }",
                    "dpi": 2
                  }
                },
                "fullPageArchive": {
                  "summary": "Full-page archival capture, delivered to a webhook",
                  "value": {
                    "url": "https://example.com/long-report",
                    "width": 1280,
                    "fullpage": true,
                    "webhook_url": "https://your-app.example.com/hooks/html2img"
                  }
                },
                "pageToPdf": {
                  "summary": "Desktop layout preserved in a PDF",
                  "value": {
                    "url": "https://example.com",
                    "format": "pdf",
                    "width": 1440,
                    "scale_to_fit": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/RenderResult"
          },
          "400": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/NotSubscribed"
          },
          "422": {
            "$ref": "#/components/responses/RenderFailed"
          },
          "500": {
            "$ref": "#/components/responses/ServiceError"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        },
        "callbacks": {
          "renderFinished": {
            "{$request.body#/webhook_url}": {
              "post": {
                "operationId": "renderWebhook",
                "summary": "Finished render delivered to your endpoint",
                "description": "POSTed once when an async render finishes, successfully or not. There is no retry, so treat a non-2xx response on your side as a lost notification and reconcile using the `log_id`, which equals the `id` from the original API response. Respond 2xx quickly and do any work off the request.",
                "requestBody": {
                  "required": true,
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/WebhookPayload"
                      }
                    }
                  }
                },
                "responses": {
                  "2XX": {
                    "description": "Any 2xx acknowledges receipt. The body is ignored."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{slug}": {
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "renderTemplate",
        "summary": "Render a named template from a JSON payload",
        "description": "Post a JSON payload to a ready-made design and skip writing markup entirely. Templates cover Open Graph and social cards, invoices, receipts, tickets, certificates, product cards, code screenshots, repository cards, property listings, quote cards and podcast art.\n\nEach template accepts its own fields. The full catalogue, and the exact field list, types and defaults for every template, is at https://html2img.com/templates/ with one page per template. Fields that a template does not recognise are rejected rather than ignored, so an unexpected 422 usually means a stray key.\n\n`format` sits outside the template's own fields because it selects the output container rather than content on the design. Template PDFs apply `scale_to_fit` automatically, so fixed-dimension artwork always lands complete on the page.\n\nValidation failures return 422 here, not 400. An unknown `slug` returns 404 `template_not_found`.",
        "externalDocs": {
          "url": "https://html2img.com/templates/"
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The template identifier, for example `open-graph-image`, `invoice`, `event-ticket`, `code-screenshot`, `github-repo-card`, `certificate-of-completion`. The full list is at https://html2img.com/templates/.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            },
            "example": "open-graph-image"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateRequest"
              },
              "examples": {
                "ogImage": {
                  "summary": "Open Graph card",
                  "value": {
                    "title": "Ship your Open Graph images",
                    "subtitle": "One POST per page, rendered in real Chrome",
                    "theme": "dark"
                  }
                },
                "invoicePdf": {
                  "summary": "Invoice as a PDF",
                  "value": {
                    "format": "pdf",
                    "invoice_number": "INV-1042",
                    "customer_name": "Acme Ltd",
                    "total": "£1,240.00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/TemplateResult"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/NotSubscribed"
          },
          "404": {
            "$ref": "#/components/responses/TemplateNotFound"
          },
          "422": {
            "$ref": "#/components/responses/TemplateUnprocessable"
          },
          "500": {
            "$ref": "#/components/responses/ServiceError"
          },
          "504": {
            "$ref": "#/components/responses/Timeout"
          }
        },
        "callbacks": {
          "renderFinished": {
            "{$request.body#/webhook_url}": {
              "post": {
                "operationId": "renderWebhook",
                "summary": "Finished render delivered to your endpoint",
                "description": "POSTed once when an async render finishes, successfully or not. There is no retry, so treat a non-2xx response on your side as a lost notification and reconcile using the `log_id`, which equals the `id` from the original API response. Respond 2xx quickly and do any work off the request.",
                "requestBody": {
                  "required": true,
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/WebhookPayload"
                      }
                    }
                  }
                },
                "responses": {
                  "2XX": {
                    "description": "Any 2xx acknowledges receipt. The body is ignored."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getAccount",
        "summary": "Read the account behind the calling key",
        "description": "Report the plan, credit balance and renewal date for the key in the `X-API-Key` header. Spends no credits and has no side effects, so it is the right call for validating a key the user has just pasted in, for a health check, or for deciding whether to attempt a render at all.\n\nDeliberately not gated on plan or balance: an account that is out of credits must still be able to read back why, so this returns 200 where a render would return 402.",
        "externalDocs": {
          "url": "https://html2img.com/docs/account/"
        },
        "responses": {
          "200": {
            "description": "The account behind the key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                },
                "examples": {
                  "paid": {
                    "summary": "Paid plan",
                    "value": {
                      "email": "you@example.com",
                      "plan": "3k",
                      "plan_name": "3,000 Credits",
                      "active": true,
                      "free_plan": false,
                      "credits_remaining": 2814,
                      "credits_reset_at": "2026-09-14T00:00:00+00:00"
                    }
                  },
                  "free": {
                    "summary": "Free tier, one-time allowance with no renewal date",
                    "value": {
                      "email": "you@example.com",
                      "plan": "free",
                      "plan_name": "50 Credits",
                      "active": true,
                      "free_plan": true,
                      "credits_remaining": 12,
                      "credits_reset_at": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/ping": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "ping",
        "summary": "Service liveness check",
        "description": "Unauthenticated liveness probe. Returns 200 whenever the API is reachable. It does not exercise the renderer, so a 200 here with failing renders means the problem is downstream, not with your key.",
        "security": [],
        "responses": {
          "200": {
            "description": "The API is reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "timestamp": "2026-08-25T09:41:07Z"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Your html2img API key, created at https://app.html2img.com/api-keys. Keys are prefixed `htim_`. Send it on every request except `GET /ping`. Keep it server-side: it is a bearer credential that spends credits, so it must never reach a browser or a mobile app bundle."
      }
    },
    "schemas": {
      "HtmlRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "html"
        ],
        "properties": {
          "html": {
            "type": "string",
            "description": "A complete HTML document to render. Inline the CSS, or pass it separately as `css`. Reference web fonts with `<link>` tags in the head; Google Fonts, Adobe Fonts and self-hosted `@font-face` all work. Inline JavaScript runs within the render budget.",
            "example": "<!doctype html><html><body><h1>Invoice #1042</h1></body></html>"
          },
          "css": {
            "type": "string",
            "description": "Extra CSS injected after the page loads, applied last so it wins over the page's own stylesheets. Use it to hide cookie banners and chat widgets, force a colour scheme, or theme one template two ways.",
            "example": ".cookie-banner, .intercom-launcher { display: none !important; }"
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "default": 1440,
            "description": "Viewport width in CSS pixels. 1200 for Open Graph and Twitter cards, 1080 for square social crops, 1280 or 1920 for desktop captures. Multiplied by `dpi` to give the final pixel width.",
            "example": 1200
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "default": 900,
            "description": "Viewport height in CSS pixels. Ignored when `fullpage` is true, and ignored for PDF output (pages are always A4 portrait). 630 for Open Graph cards, 1080 for square crops.",
            "example": 630
          },
          "fullpage": {
            "type": "boolean",
            "default": false,
            "description": "Capture the entire scroll height rather than just the viewport. `height` is ignored, and DPI is forced to 1 because full-page renders are already large. No effect on PDF output.",
            "example": false
          },
          "dpi": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "description": "Device pixel ratio multiplier. Defaults to 2 on /html and 1 on /screenshot. Use 2 for anything shown on a modern phone or laptop, 3 only for print or marketing assets. Pair 3 with `webhook_url`: the render usually exceeds the 30 second synchronous budget. Ignored for PDF output, which is vector.",
            "example": 2
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "pdf"
            ],
            "default": "png",
            "description": "Output container. `pdf` produces a real vector PDF with selectable text and embedded fonts, laid out on A4 portrait pages that paginate automatically. A PDF costs the same single credit as a PNG. With `pdf`, the parameters `dpi`, `fullpage` and `selector` have no effect.",
            "example": "png"
          },
          "scale_to_fit": {
            "type": "boolean",
            "default": false,
            "description": "PDF output only. Lays the content out at the requested `width`, then scales the whole layout down so it fits the A4 page width instead of being cropped, and trims a trailing blank page. This is how you keep a desktop layout in a PDF. Ignored for PNG.",
            "example": false
          },
          "ms_delay": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "description": "Wait this many milliseconds after load before capturing, for entry animations, lazy embeds or iframes. Prefer `wait_for_selector` when the page has a known readiness marker.",
            "example": 750
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Wait until this CSS selector appears in the DOM before capturing, with a 15 second limit. The reliable way to handle client-side charts and other JavaScript-driven content. Returns `selector_timeout` if it never appears.",
            "example": ".chart-rendered"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Switches the request to async mode. The API responds immediately with `status: \"processing\"` and POSTs the finished render to this URL. Recommended whenever a render might exceed the 30 second synchronous budget: large full-page captures, DPI 3, or JavaScript-heavy pages. Delivery is attempted once, with no retry.",
            "example": "https://your-app.example.com/hooks/html2img"
          }
        }
      },
      "ScreenshotRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The fully qualified, publicly reachable URL to load and capture. Private hosts such as `localhost` and intranet addresses cannot be reached by the renderer.",
            "example": "https://example.com"
          },
          "selector": {
            "type": "string",
            "maxLength": 255,
            "description": "Crop the capture to a single CSS selector, for example `#hero` or `main > article`. The first match wins. This endpoint only. Returns `selector_not_found` when nothing matches.",
            "example": "#hero"
          },
          "css": {
            "type": "string",
            "description": "Extra CSS injected after the page loads, applied last so it wins over the page's own stylesheets. Use it to hide cookie banners and chat widgets, force a colour scheme, or theme one template two ways.",
            "example": ".cookie-banner, .intercom-launcher { display: none !important; }"
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "default": 1440,
            "description": "Viewport width in CSS pixels. 1200 for Open Graph and Twitter cards, 1080 for square social crops, 1280 or 1920 for desktop captures. Multiplied by `dpi` to give the final pixel width.",
            "example": 1200
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "default": 900,
            "description": "Viewport height in CSS pixels. Ignored when `fullpage` is true, and ignored for PDF output (pages are always A4 portrait). 630 for Open Graph cards, 1080 for square crops.",
            "example": 630
          },
          "fullpage": {
            "type": "boolean",
            "default": false,
            "description": "Capture the entire scroll height rather than just the viewport. `height` is ignored, and DPI is forced to 1 because full-page renders are already large. No effect on PDF output.",
            "example": false
          },
          "dpi": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "description": "Device pixel ratio multiplier. Defaults to 2 on /html and 1 on /screenshot. Use 2 for anything shown on a modern phone or laptop, 3 only for print or marketing assets. Pair 3 with `webhook_url`: the render usually exceeds the 30 second synchronous budget. Ignored for PDF output, which is vector.",
            "example": 2
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "pdf"
            ],
            "default": "png",
            "description": "Output container. `pdf` produces a real vector PDF with selectable text and embedded fonts, laid out on A4 portrait pages that paginate automatically. A PDF costs the same single credit as a PNG. With `pdf`, the parameters `dpi`, `fullpage` and `selector` have no effect.",
            "example": "png"
          },
          "scale_to_fit": {
            "type": "boolean",
            "default": false,
            "description": "PDF output only. Lays the content out at the requested `width`, then scales the whole layout down so it fits the A4 page width instead of being cropped, and trims a trailing blank page. This is how you keep a desktop layout in a PDF. Ignored for PNG.",
            "example": false
          },
          "ms_delay": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "description": "Wait this many milliseconds after load before capturing, for entry animations, lazy embeds or iframes. Prefer `wait_for_selector` when the page has a known readiness marker.",
            "example": 750
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Wait until this CSS selector appears in the DOM before capturing, with a 15 second limit. The reliable way to handle client-side charts and other JavaScript-driven content. Returns `selector_timeout` if it never appears.",
            "example": ".chart-rendered"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Switches the request to async mode. The API responds immediately with `status: \"processing\"` and POSTs the finished render to this URL. Recommended whenever a render might exceed the 30 second synchronous budget: large full-page captures, DPI 3, or JavaScript-heavy pages. Delivery is attempted once, with no retry.",
            "example": "https://your-app.example.com/hooks/html2img"
          }
        }
      },
      "TemplateRequest": {
        "type": "object",
        "description": "The template's own fields, plus the optional `format`. Field names, types and defaults vary per template and are documented on each template page at https://html2img.com/templates/. Unrecognised fields are rejected with 422.",
        "additionalProperties": true,
        "properties": {
          "format": {
            "type": "string",
            "enum": [
              "png",
              "pdf"
            ],
            "default": "png",
            "description": "Output container. `pdf` produces a real vector PDF with selectable text and embedded fonts, laid out on A4 portrait pages that paginate automatically. A PDF costs the same single credit as a PNG. With `pdf`, the parameters `dpi`, `fullpage` and `selector` have no effect.",
            "example": "png"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Switches the request to async mode. The API responds immediately with `status: \"processing\"` and POSTs the finished render to this URL. Recommended whenever a render might exceed the 30 second synchronous budget: large full-page captures, DPI 3, or JavaScript-heavy pages. Delivery is attempted once, with no retry.",
            "example": "https://your-app.example.com/hooks/html2img"
          }
        }
      },
      "RenderResult": {
        "type": "object",
        "required": [
          "success",
          "id"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Always true on a 200."
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier for this render. Quote it in support requests, and use it to match an async webhook (delivered as `log_id`) back to the request that started it.",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "CDN URL of the finished render. On a synchronous request this is live when you receive it. On an async request it is the URL the render will occupy once finished: treat the webhook, not this field, as the completion signal.",
            "example": "https://i.html2img.com/image-1756100000-a1b2c3.png"
          },
          "credits_remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Credits left on the account after this call.",
            "example": 2813
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When this render will be deleted from the CDN. Null on paid plans, which keep renders permanently; set seven days out for free-tier renders. Upgrading clears it on everything not yet purged.",
            "example": null
          },
          "status": {
            "type": "string",
            "enum": [
              "processing"
            ],
            "description": "Present only when `webhook_url` was supplied. The finished render is POSTed to that URL."
          },
          "message": {
            "type": "string",
            "description": "Human-readable note accompanying `status`.",
            "example": "Image generation started"
          }
        }
      },
      "TemplateResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RenderResult"
          },
          {
            "type": "object",
            "properties": {
              "template": {
                "type": "string",
                "description": "The template slug that was rendered.",
                "example": "open-graph-image"
              }
            }
          }
        ]
      },
      "WebhookPayload": {
        "type": "object",
        "required": [
          "status",
          "log_id"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "success",
              "error"
            ],
            "description": "Whether the render finished or failed. Check this before using `url`."
          },
          "message": {
            "type": "string",
            "description": "Human-readable outcome."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "CDN URL of the finished render. Null when `status` is `error`."
          },
          "filename": {
            "type": "string",
            "example": "image-1756100000-a1b2c3.png"
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "pdf"
            ]
          },
          "dpi": {
            "type": "integer",
            "example": 2
          },
          "log_id": {
            "type": "string",
            "format": "uuid",
            "description": "Equal to the `id` returned by the original request. Use it to reconcile.",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "email",
          "active",
          "free_plan",
          "credits_remaining"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "example": "you@example.com"
          },
          "plan": {
            "type": "string",
            "nullable": true,
            "description": "Plan identifier, or null when no plan is assigned.",
            "example": "3k"
          },
          "plan_name": {
            "type": "string",
            "nullable": true,
            "example": "3,000 Credits"
          },
          "active": {
            "type": "boolean",
            "description": "Whether the account can render right now. False means renders will return 403 regardless of the balance."
          },
          "free_plan": {
            "type": "boolean",
            "description": "True on the one-time free allowance."
          },
          "credits_remaining": {
            "type": "integer",
            "minimum": 0,
            "example": 2814
          },
          "credits_reset_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the balance renews. Null on the free tier, whose allowance is one-time and does not renew."
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "timestamp"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-25T09:41:07Z"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "description": "Every failure uses this envelope: `error` is a short summary, `code` is the stable machine-readable identifier to branch on, and `message` (when present) is safe to show to your own users.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Short human-readable summary."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code. Branch on this."
          },
          "message": {
            "type": "string",
            "description": "Longer explanation, safe to surface to end users."
          }
        }
      },
      "ValidationError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "details"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "validation_error"
                ]
              },
              "details": {
                "type": "object",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "description": "Every field that failed, mapped to its messages.",
                "example": {
                  "url": [
                    "The url field is required."
                  ]
                }
              }
            }
          }
        ]
      },
      "AuthError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key"
                ],
                "description": "`missing_api_key` when no `X-API-Key` header was sent, `invalid_api_key` when the key is not recognised. Check for whitespace or truncation, and that the key has not been revoked."
              }
            }
          }
        ]
      },
      "NotSubscribedError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "not_subscribed"
                ]
              }
            }
          }
        ]
      },
      "InsufficientCreditsError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "credits_remaining"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "insufficient_credits"
                ]
              },
              "credits_remaining": {
                "type": "integer",
                "enum": [
                  0
                ]
              },
              "upgrade_url": {
                "type": "string",
                "format": "uri",
                "description": "Free accounts only: where to buy more credits."
              },
              "credits_reset_at": {
                "type": "string",
                "format": "date-time",
                "description": "Paid accounts only: when the balance renews. Retrying before then will fail the same way."
              }
            }
          }
        ]
      },
      "RenderError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "url_not_found",
                  "url_unreachable",
                  "connection_refused",
                  "page_load_timeout",
                  "selector_timeout",
                  "selector_not_found",
                  "screenshot_too_large"
                ],
                "description": "`url_not_found`: the domain would not resolve, usually a typo. `url_unreachable`: it resolved but would not load (SSL failure, reset, redirect loop). `connection_refused`: the site refused us, possibly blocking automation. `page_load_timeout`: too slow. `selector_timeout`: `wait_for_selector` never appeared. `selector_not_found`: `selector` matched nothing. `screenshot_too_large`: reduce width, height or dpi. No credit is charged for any of these."
              },
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "The render identifier, for support."
              }
            }
          }
        ]
      },
      "TemplateNotFoundError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "template_not_found"
                ]
              }
            }
          }
        ]
      },
      "TimeoutError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "timeout_error",
                  "api_timeout_error"
                ],
                "description": "`timeout_error`: the render exceeded its budget and carries an `id`. `api_timeout_error`: we timed out waiting for the renderer, with no `id`. Both mean the same fix: pass `webhook_url` and take the result asynchronously. No credit is charged."
              },
              "id": {
                "type": "string",
                "format": "uuid"
              }
            }
          }
        ]
      },
      "ServiceError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "service_error"
                ]
              },
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Quote this when reporting the failure."
              }
            }
          }
        ]
      }
    },
    "responses": {
      "RenderResult": {
        "description": "The render succeeded. A synchronous request returns the live CDN URL; a request carrying `webhook_url` returns the same envelope plus `status: \"processing\"`, and the finished render arrives at your webhook.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RenderResult"
            },
            "examples": {
              "sync": {
                "summary": "Synchronous",
                "value": {
                  "success": true,
                  "id": "550e8400-e29b-41d4-a716-446655440000",
                  "expires_at": null,
                  "credits_remaining": 2813,
                  "url": "https://i.html2img.com/image-1756100000-a1b2c3.png"
                }
              },
              "async": {
                "summary": "Async, accepted for webhook delivery",
                "value": {
                  "success": true,
                  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
                  "expires_at": null,
                  "credits_remaining": 2812,
                  "status": "processing",
                  "message": "Image generation started",
                  "url": "https://i.html2img.com/image-1756100001-d4e5f6.png"
                }
              }
            }
          }
        }
      },
      "TemplateResult": {
        "description": "The template rendered. Same envelope as the other endpoints, plus the `template` slug.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TemplateResult"
            },
            "example": {
              "success": true,
              "id": "550e8400-e29b-41d4-a716-446655440000",
              "template": "open-graph-image",
              "expires_at": null,
              "credits_remaining": 2811,
              "url": "https://i.html2img.com/image-1756100002-g7h8i9.png"
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "A parameter was missing, the wrong type or out of range. Nothing was rendered and no credit was charged. Fix the request rather than retrying it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "example": {
              "error": "Validation failed",
              "code": "validation_error",
              "details": {
                "url": [
                  "The url field is required."
                ],
                "width": [
                  "The width must be between 1 and 5000."
                ]
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The `X-API-Key` header was missing or the key is not recognised.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AuthError"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": "API key is required",
                  "code": "missing_api_key"
                }
              },
              "invalid": {
                "value": {
                  "error": "Invalid API key",
                  "code": "invalid_api_key"
                }
              }
            }
          }
        }
      },
      "InsufficientCredits": {
        "description": "The account has no credits left. Retrying will fail identically until the balance renews (paid plans, see `credits_reset_at`) or more are bought (free tier, see `upgrade_url`). There is no automatic overage charge.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/InsufficientCreditsError"
            },
            "examples": {
              "free": {
                "summary": "Free tier",
                "value": {
                  "error": "Insufficient credits",
                  "code": "insufficient_credits",
                  "credits_remaining": 0,
                  "message": "You have used your free credits. Upgrade to a paid plan to keep rendering.",
                  "upgrade_url": "https://app.html2img.com/plans"
                }
              },
              "paid": {
                "summary": "Paid plan",
                "value": {
                  "error": "Insufficient credits",
                  "code": "insufficient_credits",
                  "credits_remaining": 0,
                  "message": "Your credits renew on 2026-09-14.",
                  "credits_reset_at": "2026-09-14T00:00:00+00:00"
                }
              }
            }
          }
        }
      },
      "NotSubscribed": {
        "description": "The key is valid but the account has no active plan. Activate the free tier or subscribe at https://app.html2img.com/plans.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotSubscribedError"
            },
            "example": {
              "error": "You must be subscribed to use this service",
              "code": "not_subscribed"
            }
          }
        }
      },
      "RenderFailed": {
        "description": "The request was valid but the render could not be produced, for a reason you can fix. `message` is safe to show to your own users, and no credit was charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RenderError"
            },
            "example": {
              "error": "Render failed",
              "code": "url_not_found",
              "message": "That web address could not be found. Please check the URL for typos and make sure the site exists.",
              "id": "8a9dda43-5f42-4b93-8ff4-cd69ed32d402"
            }
          }
        }
      },
      "TemplateUnprocessable": {
        "description": "Either the payload did not satisfy the template (this endpoint returns 422 rather than 400 for validation) or the render itself failed. Branch on `code`: `validation_error` carries `details`, anything else is a render failure.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/ValidationError"
                },
                {
                  "$ref": "#/components/schemas/RenderError"
                }
              ]
            },
            "examples": {
              "validation": {
                "summary": "Payload rejected",
                "value": {
                  "error": "Validation failed",
                  "code": "validation_error",
                  "details": {
                    "title": [
                      "The title field is required."
                    ]
                  }
                }
              },
              "render": {
                "summary": "Render failed",
                "value": {
                  "error": "Render failed",
                  "code": "screenshot_too_large",
                  "message": "The requested capture exceeds the size limits.",
                  "id": "8a9dda43-5f42-4b93-8ff4-cd69ed32d402"
                }
              }
            }
          }
        }
      },
      "TemplateNotFound": {
        "description": "No template exists with that slug. The catalogue is at https://html2img.com/templates/.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TemplateNotFoundError"
            },
            "example": {
              "error": "Template not found",
              "code": "template_not_found"
            }
          }
        }
      },
      "Timeout": {
        "description": "The render exceeded the 30 second synchronous budget. Retrying synchronously will usually time out again: pass `webhook_url` instead, or reduce `dpi`, `width` or `fullpage`. No credit was charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TimeoutError"
            },
            "example": {
              "error": "Request timed out",
              "code": "timeout_error",
              "message": "Render job exceeded the allotted time. Consider passing webhook_url for large captures.",
              "id": "8a9dda43-5f42-4b93-8ff4-cd69ed32d402"
            }
          }
        }
      },
      "ServiceError": {
        "description": "A fault on our side. Retry once; if it persists, contact support quoting `id`. No credit was charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServiceError"
            },
            "example": {
              "error": "Service error",
              "code": "service_error",
              "message": "An internal error occurred while rendering.",
              "id": "8a9dda43-5f42-4b93-8ff4-cd69ed32d402"
            }
          }
        }
      }
    }
  }
}
