{
  "openapi": "3.1.0",
  "info": {
    "title": "pagenull API",
    "version": "1.0.0",
    "summary": "HTML or a URL to PDF or screenshot, with a signed receipt for every render.",
    "description": "Send HTML or a public URL, get a PDF or an image back in the same response. Every render carries an Ed25519-signed receipt (`pagenull-receipt`) over the SHA-256 of the exact output, and `pagenull-retention: none`: request content and output are not stored. Limits: html up to 5 MB, output up to 50 MB and 1000 pages, timeoutMs up to 60 s. Failed renders and test renders (`\"test\": true`) are free. Errors are always JSON: an unknown path answers 404 `not_found`, an unsupported method 405 `method_not_allowed` with an `Allow` header. Guides and examples: https://pagenull.dev/docs.",
    "termsOfService": "https://pagenull.dev/terms",
    "contact": {
      "name": "pagenull support",
      "email": "support@pagenull.dev",
      "url": "https://pagenull.dev/docs/api"
    },
    "x-logo": {
      "url": "https://pagenull.dev/icon.svg",
      "altText": "pagenull"
    }
  },
  "externalDocs": {
    "description": "API reference",
    "url": "https://pagenull.dev/docs/api"
  },
  "servers": [
    {
      "url": "https://api.pagenull.dev"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Render",
      "description": "HTML or URL to PDF or image."
    },
    {
      "name": "Templates",
      "description": "Save a layout once, render it with JSON."
    },
    {
      "name": "E-invoices",
      "description": "Factur-X / ZUGFeRD (EN 16931) from JSON."
    },
    {
      "name": "Account",
      "description": "Usage and limits."
    },
    {
      "name": "Receipts",
      "description": "Check a receipt. No key needed."
    }
  ],
  "paths": {
    "/v1/pdf": {
      "post": {
        "operationId": "createPdf",
        "tags": [
          "Render"
        ],
        "summary": "Render HTML or a URL to PDF",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (1–255 visible ASCII characters), e.g. your order id. A retry with the same key and body within 24 hours is rendered again but not charged again.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7e]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PdfRequest"
              },
              "example": {
                "html": "<h1>Hello from pagenull</h1><p>Rendered by headless Chromium.</p>",
                "format": "A4"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file itself. With `deliverTo`, the file goes to your storage instead and the answer is 201.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              },
              "content-disposition": {
                "$ref": "#/components/headers/content-disposition"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "201": {
            "description": "With `deliverTo`: the file was uploaded to your storage. Nothing is kept here.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Delivered"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_in_progress`: A request with the same Idempotency-Key is still running. Retry after Retry-After seconds.\n\n`idempotency_replay_limit`: This Idempotency-Key has been retried 5 times. Use a new key.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_in_progress",
                            "idempotency_replay_limit"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: Body larger than the limit, or html over 5 MB, or headerHtml/footerHtml over 64 KB.\n\n`output_too_large`: The output is over 50 MB or 1,000 pages.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large",
                            "output_too_large"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_mismatch`: The Idempotency-Key was already used for a different request.\n\n`blocked_url`: The URL, or a resource the page tried to load, points at a non-public address or a disallowed scheme. pagenull's own site (pagenull.dev and its subdomains) is refused too: send that page as html.\n\n`dns_failed`: url: the hostname doesn't resolve. Check the spelling, and that the domain is public.\n\n`url_unreachable`: url: the site couldn't be loaded. The message says why: connection refused or closed, no address that accepts a connection (including an attempt the network gave up on), invalid HTTPS certificate, more than 10 redirects, or a page over 20 MB.\n\n`url_http_error`: url: the site answered with an HTTP error (4xx or 5xx), so there was no page to render. The message has the status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_mismatch",
                            "blocked_url",
                            "dns_failed",
                            "url_unreachable",
                            "url_http_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.\n\n`quota_exceeded`: Monthly renders used up. Resets on the 1st (UTC).",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited",
                            "quota_exceeded"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.\n\n`render_failed`: Chromium failed for a reason on our side (a page that can't be loaded has its own code above). Not charged. Retry, and write to support if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error",
                            "render_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "`delivery_failed`: deliverTo: your storage refused the upload, or couldn't be reached. The message has its status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "delivery_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`busy`: All renderers busy. Retry after Retry-After seconds.\n\n`renderer_unavailable`: The renderer restarted or couldn't be reached during the request. Not charged. Retry after Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "busy",
                            "renderer_unavailable"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "`url_timeout`: url: the site connected but didn't answer within timeoutMs (default 30 s), or the connection took that long. Safe to retry; a larger timeoutMs (up to 60 s) may help.\n\n`timeout`: The page didn't finish in timeoutMs, or waitFor never matched.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "url_timeout",
                            "timeout"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/screenshot": {
      "post": {
        "operationId": "createScreenshot",
        "tags": [
          "Render"
        ],
        "summary": "Render HTML or a URL to PNG or JPEG",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (1–255 visible ASCII characters), e.g. your order id. A retry with the same key and body within 24 hours is rendered again but not charged again.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7e]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScreenshotRequest"
              },
              "example": {
                "url": "https://example.com",
                "width": 1280,
                "height": 800
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file itself. With `deliverTo`, the file goes to your storage instead and the answer is 201.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              },
              "content-disposition": {
                "$ref": "#/components/headers/content-disposition"
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/png"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/jpeg"
                }
              }
            }
          },
          "201": {
            "description": "With `deliverTo`: the file was uploaded to your storage. Nothing is kept here.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Delivered"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_in_progress`: A request with the same Idempotency-Key is still running. Retry after Retry-After seconds.\n\n`idempotency_replay_limit`: This Idempotency-Key has been retried 5 times. Use a new key.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_in_progress",
                            "idempotency_replay_limit"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: Body larger than the limit, or html over 5 MB, or headerHtml/footerHtml over 64 KB.\n\n`output_too_large`: The output is over 50 MB or 1,000 pages.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large",
                            "output_too_large"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_mismatch`: The Idempotency-Key was already used for a different request.\n\n`blocked_url`: The URL, or a resource the page tried to load, points at a non-public address or a disallowed scheme. pagenull's own site (pagenull.dev and its subdomains) is refused too: send that page as html.\n\n`dns_failed`: url: the hostname doesn't resolve. Check the spelling, and that the domain is public.\n\n`url_unreachable`: url: the site couldn't be loaded. The message says why: connection refused or closed, no address that accepts a connection (including an attempt the network gave up on), invalid HTTPS certificate, more than 10 redirects, or a page over 20 MB.\n\n`url_http_error`: url: the site answered with an HTTP error (4xx or 5xx), so there was no page to render. The message has the status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_mismatch",
                            "blocked_url",
                            "dns_failed",
                            "url_unreachable",
                            "url_http_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.\n\n`quota_exceeded`: Monthly renders used up. Resets on the 1st (UTC).",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited",
                            "quota_exceeded"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.\n\n`render_failed`: Chromium failed for a reason on our side (a page that can't be loaded has its own code above). Not charged. Retry, and write to support if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error",
                            "render_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "`delivery_failed`: deliverTo: your storage refused the upload, or couldn't be reached. The message has its status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "delivery_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`busy`: All renderers busy. Retry after Retry-After seconds.\n\n`renderer_unavailable`: The renderer restarted or couldn't be reached during the request. Not charged. Retry after Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "busy",
                            "renderer_unavailable"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "`url_timeout`: url: the site connected but didn't answer within timeoutMs (default 30 s), or the connection took that long. Safe to retry; a larger timeoutMs (up to 60 s) may help.\n\n`timeout`: The page didn't finish in timeoutMs, or waitFor never matched.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "url_timeout",
                            "timeout"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates": {
      "get": {
        "operationId": "listTemplates",
        "tags": [
          "Templates"
        ],
        "summary": "List your saved templates and the built-in ones",
        "responses": {
          "200": {
            "description": "Ids and names only.",
            "headers": {
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateList"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Save a template",
        "description": "HTML and sample data are encrypted at rest. Mustache syntax: `{{x}}` is HTML-escaped, `{{{x}}}` is raw.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateCreate"
              },
              "example": {
                "name": "Packing slip",
                "html": "<h1>Order {{order}}</h1><ul>{{#lines}}<li>{{qty}} × {{name}}</li>{{/lines}}</ul>",
                "sampleData": {
                  "order": "#1001",
                  "lines": [
                    {
                      "qty": 2,
                      "name": "Notebook"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved.",
            "headers": {
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateCreated"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden`: Your plan doesn't include this (e.g. a saved-template limit).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "forbidden"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: Body larger than the limit, or html over 5 MB, or headerHtml/footerHtml over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "A saved template id (`tpl_…`)."
        }
      ],
      "delete": {
        "operationId": "deleteTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Delete a saved template",
        "description": "Only this key's account's templates. Built-in templates can't be deleted.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: No such template on this account, or no such endpoint: every unknown /v1 path answers this, in JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{id}/render": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "A saved template id, or a built-in one: `invoice`, `receipt`, `certificate`, `report`."
        }
      ],
      "post": {
        "operationId": "renderTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Render a template with JSON data",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (1–255 visible ASCII characters), e.g. your order id. A retry with the same key and body within 24 hours is rendered again but not charged again.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7e]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateRenderRequest"
              },
              "example": {
                "data": {
                  "issuer": "Example Academy",
                  "recipient": "Maria Ivanova",
                  "course": "Practical TypeScript",
                  "hours": 24,
                  "date": "3 November 2026",
                  "signatory": "Alex Example",
                  "signatoryRole": "Course lead",
                  "verifyId": "EXA-7Q2M-91KD"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file itself. With `deliverTo`, the file goes to your storage instead and the answer is 201.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              },
              "content-disposition": {
                "$ref": "#/components/headers/content-disposition"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "201": {
            "description": "With `deliverTo`: the file was uploaded to your storage. Nothing is kept here.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Delivered"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: No such template on this account, or no such endpoint: every unknown /v1 path answers this, in JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_in_progress`: A request with the same Idempotency-Key is still running. Retry after Retry-After seconds.\n\n`idempotency_replay_limit`: This Idempotency-Key has been retried 5 times. Use a new key.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_in_progress",
                            "idempotency_replay_limit"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: Body larger than the limit, or html over 5 MB, or headerHtml/footerHtml over 64 KB.\n\n`output_too_large`: The output is over 50 MB or 1,000 pages.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large",
                            "output_too_large"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_mismatch`: The Idempotency-Key was already used for a different request.\n\n`blocked_url`: The URL, or a resource the page tried to load, points at a non-public address or a disallowed scheme. pagenull's own site (pagenull.dev and its subdomains) is refused too: send that page as html.\n\n`dns_failed`: url: the hostname doesn't resolve. Check the spelling, and that the domain is public.\n\n`url_unreachable`: url: the site couldn't be loaded. The message says why: connection refused or closed, no address that accepts a connection (including an attempt the network gave up on), invalid HTTPS certificate, more than 10 redirects, or a page over 20 MB.\n\n`url_http_error`: url: the site answered with an HTTP error (4xx or 5xx), so there was no page to render. The message has the status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_mismatch",
                            "blocked_url",
                            "dns_failed",
                            "url_unreachable",
                            "url_http_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.\n\n`quota_exceeded`: Monthly renders used up. Resets on the 1st (UTC).",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited",
                            "quota_exceeded"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.\n\n`render_failed`: Chromium failed for a reason on our side (a page that can't be loaded has its own code above). Not charged. Retry, and write to support if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error",
                            "render_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "`delivery_failed`: deliverTo: your storage refused the upload, or couldn't be reached. The message has its status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "delivery_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`busy`: All renderers busy. Retry after Retry-After seconds.\n\n`renderer_unavailable`: The renderer restarted or couldn't be reached during the request. Not charged. Retry after Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "busy",
                            "renderer_unavailable"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "`url_timeout`: url: the site connected but didn't answer within timeoutMs (default 30 s), or the connection took that long. Safe to retry; a larger timeoutMs (up to 60 s) may help.\n\n`timeout`: The page didn't finish in timeoutMs, or waitFor never matched.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "url_timeout",
                            "timeout"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/invoices": {
      "post": {
        "operationId": "createInvoice",
        "tags": [
          "E-invoices"
        ],
        "summary": "Create a Factur-X / ZUGFeRD e-invoice (PDF/A-3b with the EN 16931 XML embedded)",
        "description": "Send real numbers (quantities, net unit prices, VAT rates). Lines, the VAT breakdown and totals are calculated with exact decimal arithmetic, so the printed page and the XML agree.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (1–255 visible ASCII characters), e.g. your order id. A retry with the same key and body within 24 hours is rendered again but not charged again.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7e]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceRequest"
              },
              "example": {
                "invoice": {
                  "number": "INV-2026-0142",
                  "issueDate": "2026-11-03",
                  "dueDate": "2026-11-17",
                  "seller": {
                    "name": "Northwind Studio EOOD",
                    "address": {
                      "line1": "12 Example Street",
                      "city": "Sofia",
                      "postcode": "1000",
                      "country": "BG"
                    },
                    "vatId": "BG123456789"
                  },
                  "buyer": {
                    "name": "Acme Robotics GmbH",
                    "address": {
                      "line1": "Musterstraße 1",
                      "city": "Berlin",
                      "postcode": "10115",
                      "country": "DE"
                    },
                    "vatId": "DE123456789"
                  },
                  "lines": [
                    {
                      "description": "API integration, October",
                      "quantity": 12,
                      "unit": "HUR",
                      "unitPrice": "85.00",
                      "vatCategory": "AE",
                      "vatRate": 0
                    }
                  ],
                  "payment": {
                    "iban": "BG80BNBG96611020345678"
                  }
                },
                "profile": "EN16931"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file itself. With `deliverTo`, the file goes to your storage instead and the answer is 201.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              },
              "content-disposition": {
                "$ref": "#/components/headers/content-disposition"
              },
              "pagenull-einvoice": {
                "$ref": "#/components/headers/pagenull-einvoice"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "201": {
            "description": "With `deliverTo`: the file was uploaded to your storage. Nothing is kept here.",
            "headers": {
              "pagenull-retention": {
                "$ref": "#/components/headers/pagenull-retention"
              },
              "pagenull-region": {
                "$ref": "#/components/headers/pagenull-region"
              },
              "pagenull-render-id": {
                "$ref": "#/components/headers/pagenull-render-id"
              },
              "pagenull-renderer": {
                "$ref": "#/components/headers/pagenull-renderer"
              },
              "pagenull-receipt": {
                "$ref": "#/components/headers/pagenull-receipt"
              },
              "pagenull-pages": {
                "$ref": "#/components/headers/pagenull-pages"
              },
              "pagenull-warnings": {
                "$ref": "#/components/headers/pagenull-warnings"
              },
              "x-render-ms": {
                "$ref": "#/components/headers/x-render-ms"
              },
              "idempotent-replayed": {
                "$ref": "#/components/headers/idempotent-replayed"
              },
              "x-quota-limit": {
                "$ref": "#/components/headers/x-quota-limit"
              },
              "x-quota-used": {
                "$ref": "#/components/headers/x-quota-used"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Delivered"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: No such template on this account, or no such endpoint: every unknown /v1 path answers this, in JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_found"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_in_progress`: A request with the same Idempotency-Key is still running. Retry after Retry-After seconds.\n\n`idempotency_replay_limit`: This Idempotency-Key has been retried 5 times. Use a new key.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_in_progress",
                            "idempotency_replay_limit"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: Body larger than the limit, or html over 5 MB, or headerHtml/footerHtml over 64 KB.\n\n`output_too_large`: The output is over 50 MB or 1,000 pages.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large",
                            "output_too_large"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_mismatch`: The Idempotency-Key was already used for a different request.\n\n`blocked_url`: The URL, or a resource the page tried to load, points at a non-public address or a disallowed scheme. pagenull's own site (pagenull.dev and its subdomains) is refused too: send that page as html.\n\n`dns_failed`: url: the hostname doesn't resolve. Check the spelling, and that the domain is public.\n\n`url_unreachable`: url: the site couldn't be loaded. The message says why: connection refused or closed, no address that accepts a connection (including an attempt the network gave up on), invalid HTTPS certificate, more than 10 redirects, or a page over 20 MB.\n\n`url_http_error`: url: the site answered with an HTTP error (4xx or 5xx), so there was no page to render. The message has the status.\n\n`invalid_invoice`: /v1/invoices: the invoice breaks an EN 16931 rule (e.g. reverse charge without the buyer's VAT number). The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "idempotency_mismatch",
                            "blocked_url",
                            "dns_failed",
                            "url_unreachable",
                            "url_http_error",
                            "invalid_invoice"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.\n\n`quota_exceeded`: Monthly renders used up. Resets on the 1st (UTC).",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited",
                            "quota_exceeded"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.\n\n`render_failed`: Chromium failed for a reason on our side (a page that can't be loaded has its own code above). Not charged. Retry, and write to support if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error",
                            "render_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "`delivery_failed`: deliverTo: your storage refused the upload, or couldn't be reached. The message has its status.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "delivery_failed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`busy`: All renderers busy. Retry after Retry-After seconds.\n\n`renderer_unavailable`: The renderer restarted or couldn't be reached during the request. Not charged. Retry after Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "busy",
                            "renderer_unavailable"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "`url_timeout`: url: the site connected but didn't answer within timeoutMs (default 30 s), or the connection took that long. Safe to retry; a larger timeoutMs (up to 60 s) may help.\n\n`timeout`: The page didn't finish in timeoutMs, or waitFor never matched.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "url_timeout",
                            "timeout"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Account"
        ],
        "summary": "This month's usage and limits",
        "responses": {
          "200": {
            "description": "Usage for the current month (UTC).",
            "headers": {
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/x-ratelimit-limit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/x-ratelimit-remaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/x-ratelimit-reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: Something broke on our side. Not charged. Retry, and quote pagenull-render-id if it keeps happening.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/receipts/verify": {
      "servers": [
        {
          "url": "https://pagenull.dev"
        }
      ],
      "post": {
        "operationId": "verifyReceipt",
        "tags": [
          "Receipts"
        ],
        "summary": "Check a receipt's signature, and optionally a file's SHA-256",
        "description": "Send only the receipt and the hash; the file never needs to leave your machine. Nothing is stored. Limited to 30 checks a minute per address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The result. An invalid receipt is still a 200, with `valid: false` and a reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResult"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body isn't valid JSON or an option is wrong. The message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: Body larger than the limit, or html over 5 MB, or headerHtml/footerHtml over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: Too many requests this minute, for this key or for the account. Wait for Retry-After seconds.",
            "headers": {
              "retry-after": {
                "$ref": "#/components/headers/retry-after"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ],
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/pagenull-keys.json": {
      "get": {
        "operationId": "getReceiptKeys",
        "tags": [
          "Receipts"
        ],
        "summary": "The public keys receipts are signed with (JWK set)",
        "security": [],
        "responses": {
          "200": {
            "description": "Ed25519 public keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Jwks"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Account"
        ],
        "summary": "Service health",
        "security": [],
        "responses": {
          "200": {
            "description": "Database and renderer reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Something is down.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your API key: `Authorization: Bearer pm_live_…`."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "The same key in an `X-API-Key` header instead."
      }
    },
    "headers": {
      "pagenull-retention": {
        "description": "Always `none`: nothing from the request or its output was stored.",
        "schema": {
          "type": "string",
          "enum": [
            "none"
          ]
        }
      },
      "pagenull-region": {
        "description": "Where the render ran, e.g. `hetzner-helsinki`. Also signed into the receipt.",
        "schema": {
          "type": "string"
        }
      },
      "pagenull-render-id": {
        "description": "The id of this render. It's what we log; quote it to support.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "pagenull-renderer": {
        "description": "The renderer build: a SHA-256 of the renderer's code, dependency versions and Chromium version.",
        "schema": {
          "type": "string"
        }
      },
      "pagenull-receipt": {
        "description": "Ed25519-signed receipt over the render id, the SHA-256 of the output, the time, the region, the renderer build and `stored: false`. Check it at https://pagenull.dev/verify or with the key at /.well-known/pagenull-keys.json.",
        "schema": {
          "type": "string",
          "pattern": "^v1\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$"
        }
      },
      "pagenull-pages": {
        "description": "Pages in the output (1 for screenshots).",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "pagenull-warnings": {
        "description": "Only present when something looked wrong; comma-separated. The render still succeeded. The values:\n\n- `missing_glyphs:<script>`: Text in that script was drawn as empty boxes: no available font has those characters.\n- `emoji_monochrome`: Emoji fell back to a black-and-white font.\n- `font_failed:<family>`: A web font (@font-face) didn't load.\n- `content_overflow`: Something on the page is wider than the paper and will be cut off at the right edge. Checked by laying the page out at the paper's width.\n- `canvas_lowres`: A <canvas> is drawn at less than 1.5× its displayed size, so it prints blurry.\n- `external_resource_failed:<host>`: An image, stylesheet, font or script from that host didn't load.\n- `blocked_request:<host>`: Refused: the host is on a private, loopback, link-local or metadata address.\n- `self_request:<host>`: Refused: the host is pagenull itself (pagenull.dev and its subdomains). Put the file inline as a data: URL, or host it elsewhere.",
        "schema": {
          "type": "string"
        }
      },
      "pagenull-einvoice": {
        "description": "/v1/invoices only: the Factur-X profile, e.g. `factur-x; profile=EN16931; pdfa=3b`.",
        "schema": {
          "type": "string"
        }
      },
      "x-render-ms": {
        "description": "Time spent in Chromium for this render, in milliseconds.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "x-quota-limit": {
        "description": "Renders included per month on your plan.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "x-quota-used": {
        "description": "Renders counted this month, after this request.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "x-ratelimit-limit": {
        "description": "Requests allowed per minute for this key.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "x-ratelimit-remaining": {
        "description": "Requests left in the current minute for this key.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "x-ratelimit-reset": {
        "description": "Seconds until the minute resets.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "idempotent-replayed": {
        "description": "`true` when this was a retry of an earlier request with the same Idempotency-Key, rendered again and not charged again.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "content-disposition": {
        "description": "`inline; filename=\"…\"`, from `filename` (default `document`).",
        "schema": {
          "type": "string"
        }
      },
      "retry-after": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      }
    },
    "schemas": {
      "PdfRequest": {
        "type": "object",
        "properties": {
          "html": {
            "type": "string",
            "maxLength": 5242880,
            "description": "HTML to render. Up to 5 MB. Provide exactly one of html or url."
          },
          "url": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "Public http(s) URL to load. Private, loopback, link-local and metadata addresses are refused."
          },
          "javascript": {
            "default": true,
            "type": "boolean",
            "description": "Run page scripts. Turn off for untrusted HTML."
          },
          "allowNetwork": {
            "default": true,
            "type": "boolean",
            "description": "Let the page load images, fonts and CSS from public hosts. false blocks every request except the page itself."
          },
          "waitFor": {
            "description": "CSS selector to wait for before capturing, e.g. \"#chart[data-ready]\".",
            "type": "string",
            "maxLength": 512
          },
          "waitUntil": {
            "default": "load",
            "type": "string",
            "enum": [
              "load",
              "domcontentloaded",
              "networkidle"
            ],
            "description": "When the page counts as loaded."
          },
          "timeoutMs": {
            "default": 30000,
            "type": "integer",
            "minimum": 1000,
            "maximum": 60000,
            "description": "Give up after this long (1000–60000)."
          },
          "emulateMedia": {
            "default": "print",
            "type": "string",
            "enum": [
              "print",
              "screen"
            ],
            "description": "Which CSS media type applies."
          },
          "printReset": {
            "default": true,
            "type": "boolean",
            "description": "Print-friendly defaults the page can override: exact colours, table headers that repeat on every page, rows and images that don't split, headings kept with what follows. false leaves the page exactly as written."
          },
          "format": {
            "default": "A4",
            "type": "string",
            "enum": [
              "A4",
              "Letter",
              "Legal",
              "A3",
              "A5",
              "Tabloid"
            ],
            "description": "Paper size."
          },
          "width": {
            "type": "string",
            "pattern": "^(0|\\d+(\\.\\d+)?(px|in|cm|mm))$",
            "description": "Custom page size, e.g. \"80mm\" × \"200mm\" for thermal receipts. Set both; overrides format."
          },
          "height": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(0|\\d+(\\.\\d+)?(px|in|cm|mm))$"
              },
              {
                "type": "string",
                "const": "auto"
              }
            ],
            "description": "Custom page size, e.g. \"80mm\" × \"200mm\" for thermal receipts. Set both; overrides format."
          },
          "landscape": {
            "default": false,
            "type": "boolean",
            "description": "Rotate the page."
          },
          "printBackground": {
            "default": true,
            "type": "boolean",
            "description": "Print background colours and images."
          },
          "margin": {
            "default": {},
            "type": "object",
            "properties": {
              "top": {
                "type": "string",
                "pattern": "^(0|\\d+(\\.\\d+)?(px|in|cm|mm))$"
              },
              "right": {
                "type": "string",
                "pattern": "^(0|\\d+(\\.\\d+)?(px|in|cm|mm))$"
              },
              "bottom": {
                "type": "string",
                "pattern": "^(0|\\d+(\\.\\d+)?(px|in|cm|mm))$"
              },
              "left": {
                "type": "string",
                "pattern": "^(0|\\d+(\\.\\d+)?(px|in|cm|mm))$"
              }
            },
            "description": "Each a length with a unit: \"12mm\", \"0.5in\", \"1cm\", \"24px\". Without it, 12 mm on every side, set as a CSS @page rule, so margins in your own @page CSS win."
          },
          "headerHtml": {
            "type": "string",
            "maxLength": 65536,
            "description": "HTML repeated on every page. Use <span class=\"pageNumber\"> and <span class=\"totalPages\">. Set a font-size (e.g. 9px); styles from the page don't apply. Needs a matching margin."
          },
          "footerHtml": {
            "type": "string",
            "maxLength": 65536,
            "description": "HTML repeated on every page. Use <span class=\"pageNumber\"> and <span class=\"totalPages\">. Set a font-size (e.g. 9px); styles from the page don't apply. Needs a matching margin."
          },
          "scale": {
            "default": 1,
            "type": "number",
            "minimum": 0.1,
            "maximum": 2,
            "description": "0.1–2."
          },
          "pageRanges": {
            "type": "string",
            "maxLength": 100,
            "pattern": "^[\\d,\\-\\s]*$",
            "description": "e.g. \"1-3, 5\"."
          },
          "preferCssPageSize": {
            "default": true,
            "type": "boolean",
            "description": "A CSS @page size rule wins over format. Set false to make format win."
          },
          "pageNumbers": {
            "default": false,
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string",
                "maxLength": 80
              }
            ],
            "description": "Page numbers in a page margin. true prints \"Page {page} of {pages}\"; a string is your own text with {page} and {pages}, up to 80 characters."
          },
          "pageNumbersPosition": {
            "default": "bottom-right",
            "type": "string",
            "enum": [
              "bottom-right",
              "bottom-center",
              "bottom-left",
              "top-right",
              "top-center",
              "top-left"
            ],
            "description": "Where the page numbers go."
          },
          "deliverTo": {
            "type": "string",
            "maxLength": 4096,
            "format": "uri",
            "description": "A presigned PUT URL in your own storage (S3, R2, GCS, Azure, MinIO). We upload the output there, answer 201 with its size and SHA-256, and keep nothing. Redirects aren't followed; private addresses are refused."
          },
          "test": {
            "default": false,
            "type": "boolean",
            "description": "Test render: watermarked, free, doesn't count against your quota."
          },
          "filename": {
            "type": "string",
            "maxLength": 100,
            "pattern": "^[\\w.\\- ]+$",
            "description": "Name used in the Content-Disposition header."
          }
        },
        "additionalProperties": false,
        "oneOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "url"
            ]
          }
        ]
      },
      "ScreenshotRequest": {
        "type": "object",
        "properties": {
          "html": {
            "type": "string",
            "maxLength": 5242880,
            "description": "HTML to render. Up to 5 MB. Provide exactly one of html or url."
          },
          "url": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "Public http(s) URL to load. Private, loopback, link-local and metadata addresses are refused."
          },
          "javascript": {
            "default": true,
            "type": "boolean",
            "description": "Run page scripts. Turn off for untrusted HTML."
          },
          "allowNetwork": {
            "default": true,
            "type": "boolean",
            "description": "Let the page load images, fonts and CSS from public hosts. false blocks every request except the page itself."
          },
          "waitFor": {
            "description": "CSS selector to wait for before capturing, e.g. \"#chart[data-ready]\".",
            "type": "string",
            "maxLength": 512
          },
          "waitUntil": {
            "default": "load",
            "type": "string",
            "enum": [
              "load",
              "domcontentloaded",
              "networkidle"
            ],
            "description": "When the page counts as loaded."
          },
          "timeoutMs": {
            "default": 30000,
            "type": "integer",
            "minimum": 1000,
            "maximum": 60000,
            "description": "Give up after this long (1000–60000)."
          },
          "emulateMedia": {
            "default": "screen",
            "type": "string",
            "enum": [
              "print",
              "screen"
            ],
            "description": "Which CSS media type applies."
          },
          "printReset": {
            "default": true,
            "type": "boolean",
            "description": "Print-friendly defaults the page can override: exact colours, table headers that repeat on every page, rows and images that don't split, headings kept with what follows. false leaves the page exactly as written."
          },
          "type": {
            "default": "png",
            "type": "string",
            "enum": [
              "png",
              "jpeg"
            ],
            "description": "Image format."
          },
          "quality": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "JPEG quality, 1–100."
          },
          "fullPage": {
            "default": false,
            "type": "boolean",
            "description": "Capture the whole scrollable page."
          },
          "width": {
            "default": 1280,
            "type": "integer",
            "minimum": 100,
            "maximum": 4000,
            "description": "Viewport size in CSS pixels (100–4000)."
          },
          "height": {
            "default": 800,
            "type": "integer",
            "minimum": 100,
            "maximum": 4000,
            "description": "Viewport size in CSS pixels (100–4000)."
          },
          "deviceScaleFactor": {
            "default": 1,
            "type": "number",
            "minimum": 1,
            "maximum": 3,
            "description": "1–3. Use 2 for retina-sharp images."
          },
          "deliverTo": {
            "type": "string",
            "maxLength": 4096,
            "format": "uri",
            "description": "A presigned PUT URL in your own storage (S3, R2, GCS, Azure, MinIO). We upload the output there, answer 201 with its size and SHA-256, and keep nothing. Redirects aren't followed; private addresses are refused."
          },
          "test": {
            "default": false,
            "type": "boolean",
            "description": "Test render: watermarked, free, doesn't count against your quota."
          },
          "filename": {
            "type": "string",
            "maxLength": 100,
            "pattern": "^[\\w.\\- ]+$",
            "description": "Name used in the Content-Disposition header."
          }
        },
        "additionalProperties": false,
        "oneOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "url"
            ]
          }
        ]
      },
      "InvoiceRequest": {
        "type": "object",
        "properties": {
          "invoice": {
            "type": "object",
            "properties": {
              "type": {
                "default": "invoice",
                "type": "string",
                "enum": [
                  "invoice",
                  "credit_note"
                ],
                "description": "A credit note lists the credited amounts as positive numbers, and can name precedingInvoice."
              },
              "number": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100,
                "description": "Your invoice number."
              },
              "issueDate": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                "description": "Dates. supplyDate is the date of supply."
              },
              "dueDate": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                "description": "Dates. supplyDate is the date of supply."
              },
              "supplyDate": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                "description": "Dates. supplyDate is the date of supply."
              },
              "currency": {
                "default": "EUR",
                "type": "string",
                "pattern": "^[A-Z]{3}$",
                "description": "Three-letter currency code, e.g. \"EUR\", \"BGN\", \"USD\"."
              },
              "buyerReference": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "The buyer's reference (e.g. their PO number) and order number."
              },
              "orderReference": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "The buyer's reference (e.g. their PO number) and order number."
              },
              "precedingInvoice": {
                "type": "object",
                "properties": {
                  "number": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "issueDate": {
                    "type": "string",
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
                  }
                },
                "required": [
                  "number"
                ],
                "additionalProperties": false,
                "description": "For a credit note: the invoice it corrects."
              },
              "seller": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "address": {
                    "type": "object",
                    "properties": {
                      "line1": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200
                      },
                      "line2": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200
                      },
                      "city": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "postcode": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20
                      },
                      "country": {
                        "type": "string",
                        "pattern": "^[A-Z]{2}$"
                      }
                    },
                    "required": [
                      "line1",
                      "city",
                      "postcode",
                      "country"
                    ],
                    "additionalProperties": false
                  },
                  "vatId": {
                    "type": "string",
                    "pattern": "^[A-Z]{2}[0-9A-Za-z+*.]{2,13}$"
                  },
                  "legalId": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 200,
                    "format": "email",
                    "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                  }
                },
                "required": [
                  "name",
                  "address"
                ],
                "additionalProperties": false,
                "description": "address is { line1, line2, city, postcode, country } with a 2-letter country code. vatId has its country prefix, e.g. \"DE123456789\". The seller's is required unless every line is O."
              },
              "buyer": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "address": {
                    "type": "object",
                    "properties": {
                      "line1": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200
                      },
                      "line2": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200
                      },
                      "city": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "postcode": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20
                      },
                      "country": {
                        "type": "string",
                        "pattern": "^[A-Z]{2}$"
                      }
                    },
                    "required": [
                      "line1",
                      "city",
                      "postcode",
                      "country"
                    ],
                    "additionalProperties": false
                  },
                  "vatId": {
                    "type": "string",
                    "pattern": "^[A-Z]{2}[0-9A-Za-z+*.]{2,13}$"
                  },
                  "legalId": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 200,
                    "format": "email",
                    "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                  }
                },
                "required": [
                  "name",
                  "address"
                ],
                "additionalProperties": false,
                "description": "address is { line1, line2, city, postcode, country } with a 2-letter country code. vatId has its country prefix, e.g. \"DE123456789\". The seller's is required unless every line is O."
              },
              "lines": {
                "minItems": 1,
                "maxItems": 1000,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "description": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 500
                    },
                    "detail": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 1000
                    },
                    "quantity": {
                      "anyOf": [
                        {
                          "type": "number"
                        },
                        {
                          "type": "string",
                          "pattern": "^-?\\d+(\\.\\d+)?$"
                        }
                      ]
                    },
                    "unit": {
                      "default": "C62",
                      "type": "string",
                      "pattern": "^[A-Z0-9]{1,3}$"
                    },
                    "unitPrice": {
                      "anyOf": [
                        {
                          "type": "number"
                        },
                        {
                          "type": "string",
                          "pattern": "^-?\\d+(\\.\\d+)?$"
                        }
                      ]
                    },
                    "vatCategory": {
                      "default": "S",
                      "type": "string",
                      "enum": [
                        "S",
                        "Z",
                        "E",
                        "AE",
                        "K",
                        "G",
                        "O"
                      ]
                    },
                    "vatRate": {
                      "anyOf": [
                        {
                          "type": "number"
                        },
                        {
                          "type": "string",
                          "pattern": "^-?\\d+(\\.\\d+)?$"
                        }
                      ]
                    }
                  },
                  "required": [
                    "description",
                    "quantity",
                    "unitPrice",
                    "vatRate"
                  ],
                  "additionalProperties": false
                },
                "description": "Net unit price before VAT, up to 4 decimals. unit is a UN/ECE Rec 20 code: C62 (one), HUR (hour), DAY, MON, KGM …"
              },
              "payment": {
                "type": "object",
                "properties": {
                  "iban": {
                    "type": "string"
                  },
                  "bic": {
                    "type": "string",
                    "pattern": "^[A-Z0-9]{8}([A-Z0-9]{3})?$"
                  },
                  "accountName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "reference": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 140
                  },
                  "terms": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500
                  }
                },
                "additionalProperties": false,
                "description": "SEPA credit transfer details. reference defaults to the invoice number."
              },
              "paidAmount": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "string",
                    "pattern": "^-?\\d+(\\.\\d+)?$"
                  }
                ],
                "description": "Already paid; the amount due is the total minus this."
              },
              "vatExemptionReason": {
                "type": "string",
                "minLength": 1,
                "maxLength": 300,
                "description": "Printed and embedded for E, AE, K, G and O lines. The default is the category name, e.g. \"Reverse charge\"."
              },
              "note": {
                "type": "string",
                "minLength": 1,
                "maxLength": 2000,
                "description": "Free text on the invoice."
              }
            },
            "required": [
              "number",
              "issueDate",
              "seller",
              "buyer",
              "lines"
            ],
            "additionalProperties": false,
            "description": "The invoice. Fields below."
          },
          "profile": {
            "default": "EN16931",
            "type": "string",
            "enum": [
              "EN16931",
              "BASIC",
              "MINIMUM"
            ],
            "description": "Factur-X profile. EN16931 carries the full standard and is what most buyers expect. BASIC drops some optional fields. MINIMUM carries only the header and totals and doesn't count as an e-invoice in Germany."
          },
          "language": {
            "default": "en-GB",
            "type": "string",
            "maxLength": 35,
            "description": "Locale for amounts and dates on the printed side, e.g. \"de-DE\", \"fr-FR\"."
          },
          "template": {
            "default": "invoice",
            "type": "string",
            "maxLength": 64,
            "description": "Printed layout: the built-in invoice template or one of your saved template ids. It receives the computed values, already formatted."
          },
          "options": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {},
            "description": "Page options as for /v1/pdf (format, margin, pageNumbers …)."
          },
          "deliverTo": {
            "type": "string",
            "maxLength": 4096,
            "format": "uri",
            "description": "A presigned PUT URL in your own storage (S3, R2, GCS, Azure, MinIO). We upload the output there, answer 201 with its size and SHA-256, and keep nothing. Redirects aren't followed; private addresses are refused."
          },
          "test": {
            "default": false,
            "type": "boolean",
            "description": "Test render: watermarked, free, doesn't count against your quota."
          },
          "filename": {
            "type": "string",
            "maxLength": 100,
            "pattern": "^[\\w.\\- ]+$",
            "description": "Name used in the Content-Disposition header."
          }
        },
        "required": [
          "invoice"
        ],
        "additionalProperties": false
      },
      "TemplateCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "html": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1048576
          },
          "sampleData": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          },
          "options": {
            "default": {},
            "type": "object",
            "properties": {
              "format": {
                "type": "string",
                "enum": [
                  "A4",
                  "Letter",
                  "Legal",
                  "A3",
                  "A5",
                  "Tabloid"
                ]
              },
              "width": {
                "type": "string",
                "maxLength": 20
              },
              "height": {
                "type": "string",
                "maxLength": 20
              },
              "landscape": {
                "type": "boolean"
              },
              "printBackground": {
                "type": "boolean"
              },
              "margin": {
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "enum": [
                    "top",
                    "right",
                    "bottom",
                    "left"
                  ]
                },
                "additionalProperties": {
                  "type": "string",
                  "maxLength": 20
                },
                "required": [
                  "top",
                  "right",
                  "bottom",
                  "left"
                ]
              },
              "headerHtml": {
                "type": "string",
                "maxLength": 65536
              },
              "footerHtml": {
                "type": "string",
                "maxLength": 65536
              }
            },
            "additionalProperties": false
          }
        },
        "required": [
          "name",
          "html"
        ]
      },
      "TemplateRenderRequest": {
        "type": "object",
        "properties": {
          "data": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          },
          "options": {
            "default": {},
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          },
          "deliverTo": {
            "type": "string",
            "maxLength": 4096,
            "format": "uri",
            "description": "A presigned PUT URL in your own storage (S3, R2, GCS, Azure, MinIO). We upload the output there, answer 201 with its size and SHA-256, and keep nothing. Redirects aren't followed; private addresses are refused."
          },
          "test": {
            "default": false,
            "type": "boolean",
            "description": "Test render: watermarked, free, doesn't count against your quota."
          },
          "filename": {
            "type": "string",
            "maxLength": 100,
            "pattern": "^[\\w.\\- ]+$",
            "description": "Name used in the Content-Disposition header."
          }
        }
      },
      "VerifyRequest": {
        "type": "object",
        "properties": {
          "receipt": {
            "type": "string",
            "minLength": 10,
            "maxLength": 4096
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          }
        },
        "required": [
          "receipt"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "docs"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "method_not_allowed",
                  "idempotency_in_progress",
                  "idempotency_replay_limit",
                  "payload_too_large",
                  "output_too_large",
                  "idempotency_mismatch",
                  "invalid_invoice",
                  "blocked_url",
                  "dns_failed",
                  "url_unreachable",
                  "url_http_error",
                  "rate_limited",
                  "quota_exceeded",
                  "render_failed",
                  "delivery_failed",
                  "internal_error",
                  "busy",
                  "renderer_unavailable",
                  "url_timeout",
                  "timeout"
                ]
              },
              "message": {
                "type": "string",
                "description": "What went wrong, for a person to read."
              },
              "docs": {
                "type": "string",
                "const": "/docs/api#errors"
              }
            }
          }
        }
      },
      "Delivered": {
        "type": "object",
        "required": [
          "delivered",
          "status",
          "bytes",
          "sha256",
          "contentType",
          "pages"
        ],
        "properties": {
          "delivered": {
            "const": true
          },
          "status": {
            "type": "integer",
            "description": "The status your storage answered."
          },
          "bytes": {
            "type": "integer"
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "contentType": {
            "type": "string"
          },
          "pages": {
            "type": "integer"
          }
        }
      },
      "TemplateList": {
        "type": "object",
        "required": [
          "data",
          "builtIn"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "createdAt",
                "updatedAt"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "updatedAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "builtIn": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "TemplateCreated": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "period",
          "plan",
          "used",
          "limit",
          "remaining",
          "ratePerMinute"
        ],
        "properties": {
          "period": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$",
            "description": "The month, UTC."
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "growth",
              "scale",
              "volume"
            ]
          },
          "used": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          },
          "ratePerMinute": {
            "type": "integer"
          }
        }
      },
      "ReceiptPayload": {
        "type": "object",
        "required": [
          "v",
          "kid",
          "render_id",
          "sha256",
          "timestamp",
          "region",
          "renderer",
          "stored"
        ],
        "properties": {
          "v": {
            "const": 1
          },
          "kid": {
            "type": "string"
          },
          "render_id": {
            "type": "string",
            "format": "uuid"
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "region": {
            "type": "string"
          },
          "renderer": {
            "type": "string"
          },
          "stored": {
            "const": false
          }
        }
      },
      "VerifyResult": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "valid",
              "payload",
              "fileMatches"
            ],
            "properties": {
              "valid": {
                "const": true
              },
              "payload": {
                "$ref": "#/components/schemas/ReceiptPayload"
              },
              "fileMatches": {
                "type": [
                  "boolean",
                  "null"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "valid",
              "reason"
            ],
            "properties": {
              "valid": {
                "const": false
              },
              "reason": {
                "type": "string"
              }
            }
          }
        ]
      },
      "Jwks": {
        "type": "object",
        "required": [
          "keys"
        ],
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "kid",
                "kty",
                "crv",
                "alg",
                "use",
                "x"
              ],
              "properties": {
                "kid": {
                  "type": "string"
                },
                "kty": {
                  "const": "OKP"
                },
                "crv": {
                  "const": "Ed25519"
                },
                "alg": {
                  "const": "EdDSA"
                },
                "use": {
                  "const": "sig"
                },
                "x": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "db",
          "renderer"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "db": {
            "type": "string",
            "enum": [
              "ok",
              "down"
            ]
          },
          "renderer": {}
        }
      }
    }
  }
}