{
  "openapi": "3.1.0",
  "info": {
    "title": "PDFops API",
    "version": "1.0.0",
    "description": "Deterministic PDF primitives over HTTP: inspect a PDF's AcroForm fields, fill them, and merge PDFs. Edge-hosted, no Chromium. Keyless trial: 100 requests/IP/month. Free API key (250/month, emailed, no card): POST /api/signup or https://pdfops.dev/pricing. Paid tiers: Indie $16/mo (4,000), Pro $79/mo (25,000). Quotas are hard stops (429 with Retry-After) until the monthly UTC reset.",
    "contact": {
      "name": "PDFops",
      "email": "hello@pdfops.dev",
      "url": "https://pdfops.dev"
    }
  },
  "servers": [
    {
      "url": "https://pdfops.dev"
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Optional on PDF endpoints \u2014 omit for the keyless 100/IP/month trial; present = your tier quota. Invalid/revoked keys get 401 (no fallback to the IP cap)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable slug, e.g. unknown_field, invalid_api_key, rate_limited"
          },
          "details": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "details"
        ]
      },
      "InspectedField": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "checkbox",
              "dropdown",
              "radio",
              "optionlist",
              "unsupported"
            ]
          },
          "value": {
            "type": "string"
          },
          "checked": {
            "type": "boolean"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "readOnly": {
            "type": "boolean"
          },
          "raw": {
            "type": "string",
            "description": "Underlying field class, only for type=unsupported"
          }
        },
        "required": [
          "name",
          "type",
          "readOnly"
        ]
      },
      "InspectResult": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InspectedField"
            }
          },
          "fillTemplate": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Paste-ready fields object for POST /api/fill-form (supported fields only)"
          }
        },
        "required": [
          "count",
          "fields",
          "fillTemplate"
        ]
      },
      "Usage": {
        "type": "object",
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "indie",
              "pro"
            ]
          },
          "limit": {
            "type": "integer"
          },
          "used": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          },
          "period": {
            "type": "string",
            "example": "2026-07"
          },
          "resets_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "tier",
          "limit",
          "used",
          "remaining",
          "period",
          "resets_at"
        ]
      },
      "InvoiceParty": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 6
          }
        },
        "required": [
          "name"
        ]
      },
      "InvoiceItem": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "quantity": {
            "type": "number",
            "exclusiveMinimum": 0,
            "default": 1
          },
          "unit_price": {
            "type": "number",
            "minimum": 0
          }
        },
        "required": [
          "description",
          "unit_price"
        ]
      }
    }
  },
  "paths": {
    "/api/inspect": {
      "post": {
        "operationId": "inspectPdf",
        "summary": "List a PDF's AcroForm fields + get a paste-ready fill template",
        "description": "The natural first call: you can't fill fields whose names you don't know. A PDF with no AcroForm returns 200 with count 0 (not an error).",
        "security": [
          {
            "ApiKey": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "pdf": {
                    "type": "string",
                    "format": "binary"
                  }
                },
                "required": [
                  "pdf"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Field listing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InspectResult"
                }
              }
            }
          },
          "400": {
            "description": "invalid_multipart | missing_pdf | invalid_pdf",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (Retry-After header set)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/fill-form": {
      "post": {
        "operationId": "fillPdfForm",
        "summary": "Fill AcroForm fields in a PDF",
        "description": "Field names must exist in the PDF (use /api/inspect to discover them). Checkbox values are the strings \"true\"|\"false\"; dropdown/radio/optionlist values must be one of the field's options.",
        "security": [
          {
            "ApiKey": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "pdf": {
                    "type": "string",
                    "format": "binary"
                  },
                  "fields": {
                    "type": "string",
                    "description": "JSON object string mapping field name \u2192 string value"
                  }
                },
                "required": [
                  "pdf",
                  "fields"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The filled PDF",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "invalid_multipart | missing_pdf | missing_fields | invalid_fields_json | invalid_fields | invalid_pdf | no_form | invalid_field_value | unknown_field | invalid_checkbox_value | unsupported_field_type | fill_failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (Retry-After header set)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/merge": {
      "post": {
        "operationId": "mergePdfs",
        "summary": "Merge two or more PDFs into one",
        "description": "Multipart with repeated `pdf` fields; pages are concatenated in field order. AcroForm interactivity is not preserved across the merge.",
        "security": [
          {
            "ApiKey": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "pdf": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "minItems": 2
                  }
                },
                "required": [
                  "pdf"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The merged PDF",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "invalid_multipart | too_few_pdfs | invalid_pdf",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (Retry-After header set)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/signup": {
      "post": {
        "operationId": "signup",
        "summary": "Get a free API key (emailed)",
        "description": "The key (250 requests/month) is EMAILED to the address, never returned in the response \u2014 inbox possession doubles as verification. Re-signup rotates: the previous key is revoked and a fresh one is issued (this is also lost-key recovery). 5 signups/IP/day.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key issued and emailed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_json | invalid_email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited \u2014 5 signups/IP/day",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "issue_failed | email_failed (the key is revoked on send failure \u2014 safe to retry)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Current quota state for your API key",
        "description": "`used` reads the same counter enforcement increments \u2014 by construction the number the 429 fires on. Does not consume quota.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Quota state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "missing_api_key | invalid_api_key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Liveness + dependency probe",
        "responses": {
          "200": {
            "description": "Healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "turso": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Persistence down"
          }
        }
      }
    },
    "/api/invoice": {
      "post": {
        "operationId": "generateInvoice",
        "summary": "Generate a complete invoice PDF from structured data \u2014 no template needed",
        "description": "Deterministic: the same request body produces byte-identical PDF bytes (metadata dates are pinned to the invoice date), so re-rendering from webhooks/crons is idempotent. Anonymous and free-tier output carries a small 'Generated with pdfops.dev' footer line; Indie/Pro output is clean.",
        "security": [
          {
            "ApiKey": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/components/schemas/InvoiceParty"
                      }
                    ]
                  },
                  "to": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/components/schemas/InvoiceParty"
                      }
                    ]
                  },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "$ref": "#/components/schemas/InvoiceItem"
                    }
                  },
                  "invoice_number": {
                    "type": "string"
                  },
                  "date": {
                    "type": "string",
                    "description": "Shown verbatim; also pins the PDF metadata dates (determinism)"
                  },
                  "due": {
                    "type": "string"
                  },
                  "currency": {
                    "type": "string",
                    "pattern": "^[A-Z]{3}$",
                    "default": "USD"
                  },
                  "tax_rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Percent"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 1000
                  }
                },
                "required": [
                  "from",
                  "to",
                  "items"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The invoice PDF (US Letter, auto-paginating)",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "invalid_json | invalid_body | invalid_from | invalid_to | invalid_items | invalid_currency | invalid_tax_rate",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (Retry-After header set)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}