{
  "openapi": "3.1.0",
  "info": {
    "title": "Galley Render API",
    "version": "1.0.0",
    "summary": "Documents for agents. JSON in, PDF out.",
    "description": "A template plus a JSON payload becomes a PDF, PNG or JPG behind a signed URL.\n\nEvery render is deterministic and cached: the cache key is a SHA-256 over the template\nversion's checksum, the output format, and the canonical JSON of the data and options.\nAn identical repeat request returns the stored object with `cached: true` and is not billed.\n\nSmall jobs finish inside the request and return `200` with a URL. A request carrying a\n`webhook_url`, `async: true`, a large payload, or a batch item is queued and returns `202`\nwith an id to poll.\n\nAgents can skip signup entirely: the MCP server at https://mcp.galleyrender.com/mcp mints a\n50-render trial on the first call and hands back a key that works here too.",
    "termsOfService": "https://galleyrender.com/terms",
    "contact": {
      "name": "Galley Render support",
      "email": "support@galleyrender.com",
      "url": "https://galleyrender.com/support"
    }
  },
  "externalDocs": {
    "description": "Documentation",
    "url": "https://galleyrender.com/docs/quickstart"
  },
  "servers": [
    {
      "url": "https://api.galleyrender.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Renders",
      "description": "Turn a template and a payload into a file."
    },
    {
      "name": "Templates",
      "description": "The versioned template store."
    },
    {
      "name": "Account",
      "description": "Usage, limits and health."
    }
  ],
  "paths": {
    "/v1/render": {
      "post": {
        "tags": [
          "Renders"
        ],
        "operationId": "createRender",
        "summary": "Render a document",
        "description": "Renders `template` with `data` and returns a signed URL. Returns `200` when the render finished inside the request, `202` when it was queued. A cache hit always returns `200`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              },
              "examples": {
                "invoice": {
                  "summary": "A one-page invoice as a PDF",
                  "value": {
                    "template": "invoice@1",
                    "format": "pdf",
                    "data": {
                      "invoice_number": "INV-1042",
                      "issued_on": "2026-09-16",
                      "seller": {
                        "name": "Galley Render"
                      },
                      "buyer": {
                        "name": "Acme Robotics"
                      },
                      "line_items": [
                        {
                          "description": "Starter plan, September",
                          "quantity": 1,
                          "unit_price": 19
                        }
                      ]
                    }
                  }
                },
                "ogCard": {
                  "summary": "A social card as a PNG on the fast path",
                  "value": {
                    "template": "og-card",
                    "format": "png",
                    "data": {
                      "title": "JSON in, PDF out."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rendered inside the request, or served from cache.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Renders allowed per window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Renders left in this window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Render"
                }
              }
            }
          },
          "202": {
            "description": "Queued. Poll `GET /v1/renders/{id}` or wait for the webhook.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Render"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` or `asset_blocked`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error` — the key is missing, malformed or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`quota_exceeded` or `spend_cap_exceeded` — checked before the render starts, against the template's declared `expected_pages` for a PDF and 1 unit for raster output. `details.estimated_units` is what was estimated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`permission_error` — the account is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such template or version on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_error` — the payload does not match the template schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — more renders started than the plan allows per minute (60 free, 600 paid), counted per API key. `Retry-After` says how many seconds to wait; `X-RateLimit-Remaining` is on successful responses too.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`render_failed` or `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/render/batch": {
      "post": {
        "tags": [
          "Renders"
        ],
        "operationId": "createRenderBatch",
        "summary": "Render up to 50 documents",
        "description": "Every item is queued. Items that hit the cache come back finished. A failed item does not fail the batch: its entry in `renders` is an error envelope with the same `index`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              },
              "examples": {
                "two": {
                  "summary": "Two invoices with one shared webhook",
                  "value": {
                    "webhook_url": "https://example.com/hooks/galley",
                    "renders": [
                      {
                        "template": "invoice@1",
                        "format": "pdf",
                        "data": {
                          "invoice_number": "INV-1042"
                        }
                      },
                      {
                        "template": "invoice@1",
                        "format": "pdf",
                        "data": {
                          "invoice_number": "INV-1043"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Batch"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — empty batch, or more than 50 items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`. A batch spends one unit of the per-minute limit per item, so 50 renders count as 50.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/renders": {
      "get": {
        "tags": [
          "Renders"
        ],
        "operationId": "listRenders",
        "summary": "List recent renders",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many to return, newest first.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "list"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Render"
                      }
                    }
                  },
                  "required": [
                    "object",
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/renders/{id}": {
      "get": {
        "tags": [
          "Renders"
        ],
        "operationId": "getRender",
        "summary": "Get a render and a fresh signed URL",
        "description": "Signed URLs expire; the stored object does not. Call this again for a new URL rather than re-rendering — it costs nothing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The render id returned by `POST /v1/render`.",
            "schema": {
              "type": "string",
              "pattern": "^rnd_[a-z0-9]+$",
              "examples": [
                "rnd_7hq2m4x8k1bv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The render.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Render"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no render with that id on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates": {
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "listTemplates",
        "summary": "List templates on the account",
        "responses": {
          "200": {
            "description": "Every template, with its latest version number.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "list"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    }
                  },
                  "required": [
                    "object",
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "createTemplate",
        "summary": "Create a template at version 1",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateInput"
              },
              "examples": {
                "minimal": {
                  "summary": "A one-line template with a schema",
                  "value": {
                    "name": "delivery-note",
                    "engine": "chromium",
                    "source": "<html><body><h1>{{ title }}</h1></body></html>",
                    "schema": {
                      "type": "object",
                      "required": [
                        "title"
                      ],
                      "properties": {
                        "title": {
                          "type": "string"
                        }
                      }
                    },
                    "example": {
                      "title": "Delivery note 1"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — bad name, unparseable Liquid, or an invalid JSON Schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`conflict` (`details.code: \"template_exists\"`) — a live template already holds that name. Publish a new version instead. A soft-deleted template does not hold its name: deleting `invoice` frees it, and creating `invoice` again starts a new template at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{ref}": {
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "getTemplate",
        "summary": "Get one template version",
        "description": "`invoice` resolves the latest version; `invoice@3` pins version 3.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TemplateRef"
          }
        ],
        "responses": {
          "200": {
            "description": "The template and the resolved version, including its source and schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Templates"
        ],
        "operationId": "deleteTemplate",
        "summary": "Soft-delete a template",
        "description": "Existing renders keep working and their stored files are untouched, and renders pinned to `name@version` keep resolving. The name is released: creating a template with the same name afterwards succeeds and starts a new template at version 1.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TemplateRef"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "template"
                    },
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "object",
                    "id",
                    "name",
                    "deleted"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{name}/versions": {
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "listTemplateVersions",
        "summary": "List a template's versions",
        "parameters": [
          {
            "$ref": "#/components/parameters/TemplateName"
          }
        ],
        "responses": {
          "200": {
            "description": "Every published version.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "list"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TemplateVersion"
                      }
                    }
                  },
                  "required": [
                    "object",
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "createTemplateVersion",
        "summary": "Publish a new immutable version",
        "description": "Earlier versions keep rendering byte-for-byte as they did. Renders cached against the old version stay valid; the new version starts with a cold cache.\n\n**Fields you leave out are inherited from the previous version, not reset.** `engine`, `schema`, `options`, `expected_pages` and `example` all carry forward, so `{\"source\": \"…\"}` publishes a new body against the same renderer and the same validation contract. Send a field to override it, and send an empty value (`{}` for `schema` or `options`, `null` for `example`) to clear it. `source` is always required, and `message` is never inherited — it is the note for this version.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TemplateName"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateVersionInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Published.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{ref}/validate": {
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "validateTemplateData",
        "summary": "Dry-run a payload against a template schema",
        "description": "Renders nothing and costs nothing. Returns the same field-level errors `POST /v1/render` would.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TemplateRef"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "data": {
                    "description": "The payload you intend to render."
                  }
                },
                "required": [
                  "data"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether the payload is valid, and why not.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "validation"
                    },
                    "template": {
                      "type": "string",
                      "examples": [
                        "invoice@1"
                      ]
                    },
                    "valid": {
                      "type": "boolean"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FieldError"
                      }
                    },
                    "example": {
                      "description": "The template's own example payload, or null."
                    }
                  },
                  "required": [
                    "object",
                    "template",
                    "valid",
                    "errors"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getUsage",
        "summary": "Usage, cost and limits for the current period",
        "responses": {
          "200": {
            "description": "The account's position this billing period.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getAccount",
        "summary": "The account behind this key, and the webhook signing secret",
        "description": "`webhook_secret` is returned **once**, on the first call that has one to give; after that it is `null` and only `webhook_secret_prefix` identifies which secret is in use. Store it when you get it. If you lose it, rotate.",
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/webhook-secret": {
      "post": {
        "tags": [
          "Account"
        ],
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate the webhook signing secret",
        "description": "Mints a new secret and returns it once. The previous secret stops verifying immediately, so update your handler in the same change.",
        "responses": {
          "200": {
            "description": "Rotated. The new secret is in the body and will not be shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "object",
                    "account",
                    "webhook_secret",
                    "rotated_at"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook_secret"
                    },
                    "account": {
                      "type": "string"
                    },
                    "webhook_secret": {
                      "type": "string",
                      "examples": [
                        "whsec_9Fb2…"
                      ]
                    },
                    "webhook_secret_prefix": {
                      "type": "string"
                    },
                    "rotated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`authentication_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getHealth",
        "summary": "Liveness and a cheap dependency probe",
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy."
          },
          "503": {
            "description": "A dependency is unreachable."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key, `glr_sk_…`. Send it as `Authorization: Bearer glr_sk_…` or `X-API-Key: glr_sk_…`. A keyless trial token from the MCP server is an ordinary key and works here."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "parameters": {
      "TemplateRef": {
        "name": "ref",
        "in": "path",
        "required": true,
        "description": "`invoice` for the latest version, or `invoice@3` to pin one.",
        "schema": {
          "type": "string",
          "examples": [
            "invoice",
            "invoice@3"
          ]
        }
      },
      "TemplateName": {
        "name": "name",
        "in": "path",
        "required": true,
        "description": "The template name, without a version.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9][a-z0-9._-]{0,62}$",
          "examples": [
            "invoice"
          ]
        }
      }
    },
    "schemas": {
      "RenderRequest": {
        "type": "object",
        "required": [
          "template"
        ],
        "properties": {
          "template": {
            "type": "string",
            "description": "`invoice` for the latest version, or `invoice@3` to pin one. Pin it in anything you ship.",
            "examples": [
              "invoice@3"
            ]
          },
          "version": {
            "type": [
              "integer",
              "string"
            ],
            "description": "Version number, if you would rather not put `@3` in `template`."
          },
          "data": {
            "description": "The payload, validated against the template's JSON Schema.",
            "default": {}
          },
          "format": {
            "type": "string",
            "enum": [
              "pdf",
              "png",
              "jpg",
              "jpeg"
            ],
            "description": "Defaults to `pdf` for chromium templates and `png` for satori ones. `webp` is not in v1."
          },
          "options": {
            "$ref": "#/components/schemas/RenderOptions"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute https URL to POST on completion. Supplying one forces the queued path. The body is signed with `X-Galley-Signature` (HMAC-SHA256 over the raw body, using the secret from `GET /v1/account`), and delivery is retried up to five times with exponential backoff over about a quarter of an hour.",
            "examples": [
              "https://example.com/hooks/galley"
            ]
          },
          "async": {
            "type": "boolean",
            "default": false,
            "description": "Force the queued path even for a small job."
          }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "renders"
        ],
        "properties": {
          "renders": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/RenderRequest"
            }
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Applied to every item that does not carry its own."
          }
        }
      },
      "RenderOptions": {
        "type": "object",
        "description": "Merged over the template's own defaults. Options are part of the cache key, so two calls that differ only here are two different renders.",
        "properties": {
          "page_size": {
            "type": "string",
            "description": "PDF page size: `Letter`, `A4`, `Legal`, or `210mm x 297mm`."
          },
          "landscape": {
            "type": "boolean",
            "default": false,
            "description": "PDF only."
          },
          "margin": {
            "description": "PDF margins as a CSS length, or per side.",
            "oneOf": [
              {
                "type": "string",
                "examples": [
                  "18mm"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "top": {
                    "type": "string"
                  },
                  "right": {
                    "type": "string"
                  },
                  "bottom": {
                    "type": "string"
                  },
                  "left": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "description": "Viewport width in px for png/jpg. Default 1200."
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "description": "Viewport height in px for png/jpg. Default 630."
          },
          "scale": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 3,
            "description": "Device pixel ratio for png/jpg; a CSS zoom on PDF, where it is capped at 2. Values above the cap are clamped, not rejected, and the cap is 3 on both engines."
          },
          "full_page": {
            "type": "boolean",
            "description": "png/jpg: capture the whole document, not just the viewport."
          },
          "quality": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "jpg only. Default 90."
          },
          "background": {
            "type": "string",
            "description": "CSS colour painted under the document."
          },
          "print_background": {
            "type": "boolean",
            "description": "PDF: print background colours and images. Default true."
          },
          "css": {
            "type": "string",
            "description": "Extra CSS appended after the template's own styles."
          },
          "on_blocked_asset": {
            "type": "string",
            "enum": [
              "fail",
              "skip"
            ],
            "default": "fail",
            "description": "What to do when an image, font or other subresource is refused by the egress policy or fails to fetch. `fail` fails the render with `asset_blocked` naming the URL; `skip` drops the asset and renders without it. Both engines behave identically."
          }
        },
        "additionalProperties": true
      },
      "Render": {
        "type": "object",
        "required": [
          "object",
          "id",
          "status",
          "template",
          "format",
          "engine",
          "cached",
          "url",
          "created_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "render"
          },
          "id": {
            "type": "string",
            "examples": [
              "rnd_7hq2m4x8k1bv"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "succeeded",
              "failed"
            ]
          },
          "template": {
            "type": "string",
            "description": "The resolved `name@version`.",
            "examples": [
              "invoice@1"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "pdf",
              "png",
              "jpg"
            ]
          },
          "engine": {
            "type": "string",
            "enum": [
              "chromium",
              "satori"
            ]
          },
          "cached": {
            "type": "boolean",
            "description": "True when the stored object was reused. A cache hit is never billed."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Signed URL to the file. Expires in an hour by default; the stored object does not."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the stored object is swept."
          },
          "page_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pages in the PDF; 1 for raster output."
          },
          "billable_units": {
            "type": [
              "integer",
              "null"
            ],
            "description": "1 per PNG/JPG, 1 per PDF page."
          },
          "byte_size": {
            "type": [
              "integer",
              "null"
            ]
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "description": "The error envelope for a failed render, else null."
          },
          "webhook_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "pending",
              "delivered",
              "failed",
              null
            ],
            "description": "`null` when no `webhook_url` was given. `pending` while delivery is still being attempted, `delivered` once your endpoint answered 2xx, `failed` once all attempts were used."
          },
          "webhook_attempts": {
            "type": "integer",
            "description": "Delivery attempts made so far."
          },
          "batch_id": {
            "type": "string",
            "description": "Present only for batch items."
          }
        }
      },
      "Batch": {
        "type": "object",
        "required": [
          "object",
          "id",
          "count",
          "queued",
          "cached",
          "renders"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "batch"
          },
          "id": {
            "type": "string",
            "examples": [
              "bat_3k9dm2p1xq4v"
            ]
          },
          "count": {
            "type": "integer"
          },
          "queued": {
            "type": "integer"
          },
          "cached": {
            "type": "integer"
          },
          "renders": {
            "type": "array",
            "description": "One entry per item, in order, each carrying its `index`. A failed item is an error envelope.",
            "items": {}
          }
        }
      },
      "TemplateInput": {
        "type": "object",
        "required": [
          "name",
          "source"
        ],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9._-]{0,62}$",
            "examples": [
              "invoice"
            ]
          },
          "source": {
            "type": "string",
            "description": "One self-contained HTML document with inline CSS and Liquid expressions. No file includes."
          },
          "engine": {
            "type": "string",
            "enum": [
              "chromium",
              "satori"
            ],
            "default": "chromium"
          },
          "schema": {
            "type": "object",
            "description": "JSON Schema for the `data` payload. Strongly recommended: it is what turns a bad payload into a field-level error."
          },
          "options": {
            "$ref": "#/components/schemas/RenderOptions"
          },
          "expected_pages": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2000,
            "default": 1,
            "description": "How many PDF pages a typical payload renders. This is the pre-flight estimate the free tier and the spend cap are checked against, so a multi-page template must declare it or a render that cannot fit the allowance will be allowed to start. It is not a limit: the render is metered on the pages it actually produced, and an under-estimate is corrected afterwards from that count. Ignored for png/jpg, which are always one unit."
          },
          "example": {
            "description": "A payload that renders correctly. Echoed back in validation errors."
          },
          "description": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Change note for this version, like a commit message."
          }
        }
      },
      "TemplateVersionInput": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TemplateInput"
          }
        ],
        "description": "Same body as creating a template, minus `name` — the name comes from the path. Only `source` is required: `engine`, `schema`, `options`, `expected_pages` and `example` are inherited from the previous version unless this body sets them."
      },
      "Template": {
        "type": "object",
        "required": [
          "object",
          "id",
          "name",
          "latest_version",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "template"
          },
          "id": {
            "type": "string",
            "examples": [
              "tpl_5m2k8q1xv7bd"
            ]
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "latest_version": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "version": {
            "type": "integer",
            "description": "Present when a version was resolved."
          },
          "ref": {
            "type": "string",
            "examples": [
              "invoice@1"
            ]
          },
          "engine": {
            "type": "string",
            "enum": [
              "chromium",
              "satori"
            ]
          },
          "schema": {
            "type": [
              "object",
              "null"
            ]
          },
          "options": {
            "type": [
              "object",
              "null"
            ]
          },
          "expected_pages": {
            "type": "integer",
            "description": "The version's declared PDF page estimate."
          },
          "example": {},
          "source": {
            "type": "string"
          },
          "checksum": {
            "type": "string",
            "description": "SHA-256 of engine, source, schema and options. Part of the render cache key."
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "version_created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateVersion": {
        "type": "object",
        "required": [
          "object",
          "id",
          "template",
          "ref",
          "version",
          "engine",
          "checksum",
          "created_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "template_version"
          },
          "id": {
            "type": "string"
          },
          "template": {
            "type": "string"
          },
          "ref": {
            "type": "string",
            "examples": [
              "invoice@3"
            ]
          },
          "version": {
            "type": "integer"
          },
          "engine": {
            "type": "string",
            "enum": [
              "chromium",
              "satori"
            ]
          },
          "expected_pages": {
            "type": "integer"
          },
          "checksum": {
            "type": "string"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "object",
          "account",
          "plan",
          "period",
          "renders",
          "billable_units",
          "cost_usd",
          "limits",
          "prices"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "usage"
          },
          "account": {
            "type": "string",
            "examples": [
              "acct_hj8kpw14xk1v"
            ]
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "payg",
              "starter",
              "growth",
              "scale"
            ]
          },
          "period": {
            "type": "string",
            "description": "`YYYY-MM`, UTC.",
            "examples": [
              "2026-09"
            ]
          },
          "period_resets_on": {
            "type": "string",
            "format": "date"
          },
          "renders": {
            "type": "integer"
          },
          "billable_units": {
            "type": "integer"
          },
          "by_format": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "billable_units": {
                  "type": "integer"
                },
                "cost_usd": {
                  "type": "number"
                }
              }
            }
          },
          "cost_usd": {
            "type": "number"
          },
          "limits": {
            "type": "object",
            "properties": {
              "free_renders_per_month": {
                "type": "integer"
              },
              "free_renders_remaining": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "monthly_spend_cap_usd": {
                "type": "number"
              },
              "spend_remaining_usd": {
                "type": "number"
              }
            }
          },
          "trial": {
            "type": [
              "object",
              "null"
            ],
            "description": "Present only on a keyless trial account.",
            "properties": {
              "renders_limit": {
                "type": "integer"
              },
              "renders_used": {
                "type": "integer"
              },
              "renders_remaining": {
                "type": "integer"
              },
              "upgrade": {
                "type": "string"
              }
            }
          },
          "prices": {
            "type": "object",
            "properties": {
              "png_usd": {
                "type": "number",
                "examples": [
                  0.006
                ]
              },
              "pdf_page_usd": {
                "type": "number",
                "examples": [
                  0.015
                ]
              },
              "meter_event_name": {
                "type": "string",
                "examples": [
                  "galley_render"
                ]
              }
            }
          },
          "lifetime_renders": {
            "type": "integer"
          },
          "current_period": {
            "type": "string"
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "object",
          "id",
          "plan",
          "status"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "account"
          },
          "id": {
            "type": "string",
            "examples": [
              "acct_hj8kpw14xk1v"
            ]
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null until the address is verified."
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "payg",
              "starter",
              "growth",
              "scale"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended"
            ]
          },
          "verified": {
            "type": "boolean"
          },
          "trial": {
            "type": "boolean"
          },
          "webhook_secret": {
            "type": [
              "string",
              "null"
            ],
            "description": "Shown once, on the first call. Null afterwards — rotate if you no longer have it.",
            "examples": [
              "whsec_9Fb2…"
            ]
          },
          "webhook_secret_prefix": {
            "type": "string",
            "description": "Identifies the stored secret without revealing it."
          },
          "webhook_secret_shown_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "webhook_signature_header": {
            "type": "string",
            "const": "X-Galley-Signature"
          },
          "webhook_signature_scheme": {
            "type": "string",
            "examples": [
              "sha256=<hex hmac-sha256 of the raw request body>"
            ]
          },
          "rate_limit": {
            "type": "object",
            "description": "Renders this account's keys may start per window. Counted per API key.",
            "properties": {
              "renders_per_minute": {
                "type": "integer",
                "examples": [
                  60
                ]
              },
              "window_seconds": {
                "type": "integer",
                "examples": [
                  60
                ]
              }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "free_renders_per_month": {
                "type": "integer"
              },
              "monthly_spend_cap_usd": {
                "type": "number"
              }
            }
          }
        }
      },
      "FieldError": {
        "type": "object",
        "required": [
          "path",
          "message",
          "expected"
        ],
        "description": "Written so a caller can fix the payload without guessing.",
        "properties": {
          "path": {
            "type": "string",
            "description": "Dotted path into the submitted data.",
            "examples": [
              "data.line_items[1].quantity"
            ]
          },
          "message": {
            "type": "string"
          },
          "expected": {
            "type": "string",
            "description": "The type or constraint the field must satisfy.",
            "examples": [
              "number"
            ]
          },
          "received": {
            "type": "string",
            "description": "What arrived, described briefly.",
            "examples": [
              "string"
            ]
          },
          "example": {
            "description": "A value that would be accepted."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "type",
              "message",
              "docs_url"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "validation_error",
                  "authentication_error",
                  "permission_error",
                  "not_found",
                  "conflict",
                  "quota_exceeded",
                  "spend_cap_exceeded",
                  "rate_limited",
                  "asset_blocked",
                  "render_failed",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "docs_url": {
                "type": "string",
                "format": "uri",
                "examples": [
                  "https://galleyrender.com/docs/errors/validation_error"
                ]
              },
              "errors": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FieldError"
                }
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      }
    }
  }
}