{
  "openapi": "3.1.0",
  "info": {
    "title": "PrintPreflight API",
    "version": "1.0.0",
    "description": "Automated print-ready PDF preflight checking. Submit artwork, get detailed checks for trim/bleed compliance, DPI, colour space, fonts, and more."
  },
  "servers": [
    {
      "url": "https://api.preflight-api.com",
      "description": "Production"
    },
    {
      "url": "https://bjchkbkfkpmsuqvuxlwp.supabase.co/functions/v1/api-jobs",
      "description": "Direct Edge Function endpoint"
    }
  ],
  "security": [
    { "ApiKeyAuth": [] }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "schemas": {
      "TrimSize": {
        "type": "object",
        "required": ["width", "height"],
        "properties": {
          "width": { "type": "number", "example": 210 },
          "height": { "type": "number", "example": 297 }
        }
      },
      "BleedSpec": {
        "type": "object",
        "properties": {
          "left": { "type": "number", "example": 3 },
          "right": { "type": "number", "example": 3 },
          "top": { "type": "number", "example": 3 },
          "bottom": { "type": "number", "example": 3 }
        }
      },
      "SafeZoneSpec": {
        "type": "object",
        "properties": {
          "left": { "type": "number", "example": 5 },
          "right": { "type": "number", "example": 5 },
          "top": { "type": "number", "example": 5 },
          "bottom": { "type": "number", "example": 5 }
        }
      },
      "PageSpec": {
        "type": "object",
        "description": "Defines the expected dimensions and margins for a range of pages in the PDF. Use multiple PageSpec entries in the pages array to handle documents where different pages have different sizes (e.g. cover vs text pages in a perfect bound book).",
        "required": ["type", "range", "trim"],
        "properties": {
          "type": { "type": "string", "enum": ["combined", "front", "back"], "description": "Whether the artwork contains both sides in one file (combined) or is split into separate front/back files.", "example": "combined" },
          "range": { "type": "string", "description": "Which pages this spec applies to. Examples: \"1\" (page 1 only), \"1-4\" (pages 1–4), \"2-100\", \"all\".", "example": "1-4" },
          "trim": { "$ref": "#/components/schemas/TrimSize" },
          "bleed": { "$ref": "#/components/schemas/BleedSpec" },
          "safe_zone": { "$ref": "#/components/schemas/SafeZoneSpec", "description": "Inset from the trim edge where important content (text, logos) should stay to avoid being cut." }
        }
      },
      "SubmitJobRequest": {
        "type": "object",
        "required": ["artwork", "spec"],
        "properties": {
          "job_id": { "type": "string", "description": "Optional external job ID for tracking" },
          "artwork": {
            "type": "object",
            "required": ["url", "filename"],
            "properties": {
              "url": { "type": "string", "format": "uri", "example": "https://example.com/artwork.pdf" },
              "filename": { "type": "string", "example": "business-card.pdf" }
            }
          },
          "webhook": {
            "type": "object",
            "properties": {
              "url": { "type": "string", "format": "uri", "example": "https://yourapp.com/webhook" },
              "secret": { "type": "string", "description": "HMAC secret for verifying webhook signatures" }
            }
          },
          "proof": {
            "type": "object",
            "description": "Controls visual proof generation. When enabled, a proof image is generated and a URL is returned in the webhook callback and job response. To serve proofs under your own domain, embed the returned proof_url in an iframe on your site (see x-proof-whitelabel).",
            "properties": {
              "generate": { "type": "boolean", "default": true, "description": "Whether to generate a visual proof image for this job." },
              "expires_hours": { "type": "integer", "default": 72, "description": "How many hours the proof link remains valid." },
              "base_url": { "type": "string", "description": "Custom base URL for proof viewer links. If provided, the proof_url in callbacks will be constructed using this base URL instead of the default." }
            }
          },
          "spec": {
            "type": "object",
            "required": ["units", "pages"],
            "properties": {
              "units": { "type": "string", "enum": ["mm", "in"], "default": "mm" },
              "pages": { "type": "array", "items": { "$ref": "#/components/schemas/PageSpec" } },
              "page_count": {
                "type": "object",
                "properties": {
                  "min": { "type": "integer", "example": 1 },
                  "max": { "type": "integer", "example": 100 },
                  "must_be_even": { "type": "boolean", "default": false }
                }
              },
              "min_dpi": { "type": "integer", "default": 300 },
              "colour_space": { "type": "string", "enum": ["any", "CMYK", "RGB"], "default": "any" },
              "font_check": { "type": "boolean", "default": true },
              "dimension_tolerance_mm": { "type": "number", "default": 0.5 }
            }
          }
        }
      },
      "JobCheck": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "example": "Trim Size" },
          "status": { "type": "string", "enum": ["pass", "fail", "warn"], "example": "pass" },
          "details": { "type": "string", "example": "210 x 297 mm — matches spec" }
        }
      },
      "JobResponse": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string", "enum": ["queued", "processing", "completed", "failed"] },
          "result": { "type": "string", "enum": ["pass", "fail", "error"] },
          "filename": { "type": "string" },
          "submitted_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": "string", "format": "date-time" },
          "processing_time": { "type": "string", "example": "1.2s" },
          "checks": { "type": "array", "items": { "$ref": "#/components/schemas/JobCheck" } },
          "proof_url": { "type": "string", "format": "uri" }
        }
      },
      "WebhookPayload": {
        "type": "object",
        "properties": {
          "event": { "type": "string", "enum": ["job.completed", "job.failed"] },
          "job_id": { "type": "string" },
          "status": { "type": "string" },
          "result": { "type": "string" },
          "checks": { "type": "array", "items": { "$ref": "#/components/schemas/JobCheck" } },
          "proof_url": { "type": "string" },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "detail": { "type": "string" }
        }
      }
    }
  },
  "paths": {
    "/v1/jobs": {
      "post": {
        "summary": "Submit a preflight job",
        "operationId": "submitJob",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SubmitJobRequest" },
              "examples": {
                "simple": {
                  "summary": "Single page spec (business card)",
                  "value": {
                    "artwork": { "url": "https://example.com/artwork.pdf", "filename": "business-card.pdf" },
                    "spec": {
                      "units": "mm",
                      "pages": [{ "type": "combined", "range": "1", "trim": { "width": 90, "height": 55 }, "bleed": { "left": 3, "right": 3, "top": 3, "bottom": 3 } }],
                      "min_dpi": 300,
                      "colour_space": "CMYK"
                    }
                  }
                },
                "perfectBoundBook": {
                  "summary": "Multi-page spec (perfect bound A4 book)",
                  "value": {
                    "artwork": { "url": "https://example.com/book.pdf", "filename": "book-a4.pdf" },
                    "spec": {
                      "units": "mm",
                      "pages": [
                        { "type": "combined", "range": "1", "trim": { "width": 425, "height": 297 }, "bleed": { "left": 3, "right": 3, "top": 3, "bottom": 3 }, "safe_zone": { "left": 10, "right": 10, "top": 10, "bottom": 10 } },
                        { "type": "combined", "range": "2-100", "trim": { "width": 210, "height": 297 }, "bleed": { "left": 3, "right": 3, "top": 3, "bottom": 3 }, "safe_zone": { "left": 5, "right": 5, "top": 5, "bottom": 5 } }
                      ],
                      "min_dpi": 300,
                      "colour_space": "CMYK"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": { "type": "string" },
                    "status": { "type": "string", "example": "queued" },
                    "created_at": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "summary": "List jobs",
        "operationId": "listJobs",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["queued", "processing", "completed", "failed"] } },
          { "name": "search", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Job list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/JobResponse" } },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/jobs/{job_id}": {
      "get": {
        "summary": "Get job details",
        "operationId": "getJob",
        "parameters": [
          { "name": "job_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Job details",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobResponse" }
              }
            }
          },
          "404": { "description": "Job not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
      }
    }
  },
  "x-proof-whitelabel": {
    "title": "White-Label Proof URLs",
    "description": "By default, proof URLs point to the PrintPreflight proxy. To serve proofs under your own domain, create a page on your site that embeds the proof_url from the webhook callback in an iframe.\n\n### Step 1 — Create a proof page on your domain\n\nAdd a route (e.g. `/proof/:token`) that renders a full-screen iframe:\n\n```html\n<iframe\n  src=\"{proof_url_from_callback}\"\n  style=\"width:100%; height:100vh; border:none;\"\n  title=\"Artwork Proof\"\n/>\n```\n\n### Step 2 — Handle the webhook callback\n\nWhen your webhook receives a `job.completed` event, extract `proof_url` from the payload. This URL is already proxied and CORS-enabled. Build your customer-facing link by combining your domain with the token:\n\n```\nhttps://yourdomain.com/proof/{token}\n```\n\n### Step 3 (Optional) — Set base_url in job submission\n\nInclude `proof.base_url` when submitting a job to have the API construct proof links using your domain automatically:\n\n```json\n{\n  \"proof\": {\n    \"generate\": true,\n    \"base_url\": \"https://yourdomain.com/proof\"\n  }\n}\n```\n\n### Example: React/Next.js proof page\n\n```tsx\nimport { useParams } from 'react-router-dom';\n\nexport default function ProofPage() {\n  const { token } = useParams();\n  const proofSrc = `https://bjchkbkfkpmsuqvuxlwp.supabase.co/functions/v1/proof-proxy?token=${token}`;\n  return <iframe src={proofSrc} className=\"w-full h-screen border-0\" title=\"Artwork Proof\" />;\n}\n```\n\nThis way, your customers see `https://yourdomain.com/proof/abc123` instead of a third-party URL."
  }
}
}
