openapi: 3.1.0
info:
  title: Airpdf Render API
  version: 1.0.0
  summary: Generate PDFs from JSON-tree templates.
  description: |
    Airpdf is a PDF generation API for modern SaaS. Templates are designed
    in a visual editor, stored as a typed JSON tree, and rendered by a
    stateless Node renderer pool. No headless Chrome.

    See the public docs for guides, SDK references, and full error codes:
    <https://airpdf.app/docs>.
  contact:
    name: Airpdf support
    email: hello@airpdf.app
    url: https://airpdf.app
  license:
    name: Proprietary
    url: https://airpdf.app/terms

servers:
  - url: https://airpdf.app/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Render
    description: Generate PDFs from a template + data payload.
  - name: Templates
    description: List and inspect templates available in your workspace.

paths:
  /render:
    post:
      tags: [Render]
      operationId: render
      summary: Render a PDF from a template
      description: |
        Synchronous by default — the API returns the rendered PDF (binary or
        URL) within 25 seconds. Pass `"async": true` to enqueue the render
        and receive the result via webhook.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RenderRequest"
            examples:
              simple:
                summary: Sync render — binary response
                value:
                  template: invoice
                  data:
                    customer: { name: "Acme Inc." }
                    items: [{ label: "Pro plan", amount: 99 }]
                  response: binary
              url:
                summary: Sync render — presigned URL response
                value:
                  template: invoice
                  data: { customer: { name: "Acme Inc." } }
                  response: url
              async:
                summary: Async render with webhook callback
                value:
                  template: monthly-report
                  data: { period: "2026-04" }
                  async: true
                  webhook_url: "https://example.com/airpdf/webhooks"
      responses:
        "200":
          description: Synchronous render succeeded.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                $ref: "#/components/schemas/RenderURL"
        "202":
          description: Async render accepted; result delivered via webhook.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RenderAsyncAccepted"
        "400":
          description: Invalid request payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          description: Free-tier monthly quota exceeded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Data payload failed JSON Schema validation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationError"
        "429":
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              schema: { type: integer }
            X-RateLimit-Remaining:
              schema: { type: integer }
            Retry-After:
              schema: { type: integer }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Renderer error or internal failure (idempotent retry safe).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /render/{id}:
    get:
      tags: [Render]
      operationId: getRender
      summary: Get the status of an async render
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Render status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RenderStatus"
        "404":
          description: Render not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /templates:
    get:
      tags: [Templates]
      operationId: listTemplates
      summary: List active templates in your workspace
      responses:
        "200":
          description: List of templates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Template"

  /templates/{slug}:
    get:
      tags: [Templates]
      operationId: getTemplate
      summary: Get a template + its variables JSON Schema
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Template with variables_schema.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateDetail"
        "404":
          description: Template not found.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "airpdf_live_*** | airpdf_test_***"
      description: |
        Pass your API key in the `Authorization` header.

        Keys come in two flavors:
          * `airpdf_live_…` — production. Counts against your monthly quota.
          * `airpdf_test_…` — sandbox. Free, never counts against quota.

  schemas:
    RenderRequest:
      type: object
      required: [template, data]
      properties:
        template:
          type: string
          description: Slug of the template to render.
          example: invoice
        data:
          type: object
          description: Payload validated against the template's `variables_schema`.
          additionalProperties: true
        response:
          type: string
          enum: [binary, url]
          default: binary
          description: |
            `binary` (default) returns the PDF directly with
            `Content-Type: application/pdf`. `url` returns a JSON object
            containing a presigned URL (1h TTL).
        async:
          type: boolean
          default: false
          description: Enqueue render and deliver result via webhook.
        webhook_url:
          type: string
          format: uri
          description: Required when `async` is true.
        filename:
          type: string
          description: Output filename (used in `Content-Disposition` and webhook payload).
        idempotency_key:
          type: string
          description: |
            Optional — server treats two requests with the same key as a
            single render. Available on paid plans.

    RenderURL:
      type: object
      required: [id, url, expires_at]
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
          description: Presigned URL (1h TTL by default).
        expires_at:
          type: string
          format: date-time
        bytes:
          type: integer
        renderer_version:
          type: string

    RenderAsyncAccepted:
      type: object
      required: [id, status, poll_url]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, rendering]
        poll_url:
          type: string
          format: uri
          example: https://airpdf.app/api/v1/render/8b1f…

    RenderStatus:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, rendering, succeeded, failed, cancelled]
        url:
          type: string
          nullable: true
          format: uri
        bytes:
          type: integer
          nullable: true
        duration_ms:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true

    Template:
      type: object
      required: [slug, name, status]
      properties:
        slug:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        status:
          type: string
          enum: [draft, active, archived]
        renderer_version:
          type: string
        thumbnail_url:
          type: string
          nullable: true
        updated_at:
          type: string
          format: date-time

    TemplateDetail:
      allOf:
        - $ref: "#/components/schemas/Template"
        - type: object
          properties:
            variables_schema:
              type: object
              description: JSON Schema describing the expected `data` payload.
              additionalProperties: true

    Error:
      type: object
      required: [error_code, message]
      properties:
        error_code:
          type: string
          example: template_not_found
        message:
          type: string
          example: "No template with slug 'invoice' in this workspace."
        request_id:
          type: string
          description: Echo this in support requests.

    ValidationError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            errors:
              type: array
              items:
                type: object
                properties:
                  path: { type: string, example: "/items/0/amount" }
                  message: { type: string, example: "must be a number" }
