{"openapi":"3.1.0","info":{"title":"preflight.art API","version":"1.0.0","description":"REST API for programmatic PDF preflight analysis on packaging and label artwork.\n\nUpload a PDF, .ai, or .eps; poll for completion; download the structured findings and a rendered PDF report. Authentication is via the `x-api-key` header. Access to v1 is currently limited to enterprise and partner accounts — contact hello@preflight.art if you need access.\n\n**Pipeline overview:** every submission runs (1) deterministic extraction across 4 PDF inspection layers, (2) a Claude-driven analysis pass that produces structured findings, (3) a vision audit that catches visual issues the deterministic layer misses, and (4) plate-level analysis for press recommendations. Typical completion time is 60–120 seconds depending on file complexity.\n\n**Bring-your-own Anthropic key (optional):** you can attach your own Anthropic API key to a preflight key so the analysis runs on your Anthropic billing rather than ours. Contact support to enable.","contact":{"name":"preflight.art support","email":"hello@preflight.art","url":"https://preflight.art"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://backend-staging-staging-f9a5.up.railway.app","description":"Current environment"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Preflight","description":"Submit, poll, and retrieve preflight analyses."},{"name":"Profiles","description":"List supported analysis profiles."},{"name":"Webhooks","description":"Outbound delivery notifications when a job completes or fails. Signed with HMAC-SHA256."},{"name":"Intake links","description":"Per-quote artwork links: a hosted drop page per quote, one webhook per upload, signed download links. See /v1/intake-guide."}],"paths":{"/v1/profiles":{"get":{"tags":["Profiles"],"summary":"List available preflight profiles","description":"Returns the set of profiles the API supports. The `id` value is what you pass as the `profile` form field on POST /v1/preflight.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"Profile catalog","content":{"application/json":{"schema":{"type":"object","properties":{"profiles":{"type":"array","items":{"$ref":"#/components/schemas/Profile"}}},"required":["profiles"]},"example":{"profiles":[{"id":"digital-labels","name":"Digital Labels","description":"Digital press labels — HP Indigo, Xeikon, inkjet","printMethod":"digital"},{"id":"flexo-labels","name":"Flexo Labels","description":"Pressure sensitive labels, flexographic printing","printMethod":"flexo"},{"id":"flexpack","name":"Flexible Packaging","description":"Stand-up pouches, flat pouches, roll stock","printMethod":"flexpack"},{"id":"cartons-folding","name":"Folding Cartons","description":"SBS / CCNB / FBB paperboard cartons","printMethod":"offset-litho"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/preflight":{"post":{"tags":["Preflight"],"summary":"Upload a file and start a preflight analysis","description":"Returns immediately with a `job_id`. Poll GET /v1/preflight/{id}/status until `status` is `complete` or `error`, then call GET /v1/preflight/{id}/report.\n\nFile constraints: 75 MB maximum, .pdf / .ai / .eps only. PDFs must not be password-protected.\n\nThe `profile` field defaults to `digital-labels` if omitted. Use GET /v1/profiles to discover all supported values.\n\n**Processing depth (`depth`)** — `full` (default) runs the complete deep prepress report (AI + vision + plate analysis, ~60–120s). `depth=quick` runs a fast deterministic intake screen (color space, image resolution, embedded vs. linked images, fonts, page-box bleed, named-dieline presence) in a few seconds with no AI calls — ideal for an automated accept/reject gate (e.g. a hot folder). Quick reports carry the same `findings` shape, each with a `fix` field, plus a `quick_verdict` (`PASS` / `FIX_REQUIRED` / `UNREADABLE`). The PDF report and completion webhook are produced for both depths.","security":[{"ApiKeyAuth":["preflight:write"]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"source_url":{"type":"string","format":"uri","maxLength":2048,"description":"Instead of `file`: an https download link. We fetch the file and run it exactly as an upload. The link must download the file with no login (a public or pre-signed link, not a preview page). Same size limit and file types as `file`. Send one of `file` or `source_url`, not both. Also accepted in a JSON body."},"source_filename":{"type":"string","description":"Optional, with `source_url`: the file name to use when the link does not carry one (for example `front.pdf`)."},"file":{"type":"string","format":"binary","description":"The artwork: PDF, .ai, .eps, .png/.jpg, or a .zip. A zip with ONE artwork is read as an Illustrator package; a zip with several becomes one job per artwork (the 202 then adds upload_id, job_ids, files and skipped, and a single upload.completed webhook fires when all are done)."},"profile":{"type":"string","description":"Profile id from GET /v1/profiles. Defaults to `digital-labels`.","enum":["cartons-folding","cartons-folding-digital","digital-flexpack","digital-labels","flexo-labels","flexpack","gravure-flexpack","offset-flexpack"],"default":"digital-labels"},"job_description":{"type":"string","description":"Optional free-text job notes — substrate, dimensions, two-surface print, special handling. Improves the engine's heuristics. Max 4000 chars.","maxLength":4000},"depth":{"type":"string","description":"Processing depth. `full` (default) = complete deep prepress report. `quick` = fast deterministic intake screen (seconds, no AI calls) returning the same findings shape with `fix` guidance plus a `quick_verdict`.","enum":["full","quick"],"default":"full"},"reference":{"type":"string","maxLength":128,"description":"Your own id for this work (a quote or PO number). Echoed on the status, report and every webhook for the job. Refused with REFERENCE_TOO_LONG, never truncated."},"metadata":{"type":"string","description":"JSON object of flat string/number/boolean values, at most 2 KB and 32 keys. Echoed on every webhook for the job."}}}}}},"responses":{"202":{"description":"Job accepted and processing started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"},"example":{"job_id":"4c68c919-1ded-43b4-916e-f9e97ea4ce0e","status":"processing","profile_id":"flexo-labels","depth":"quick","poll_url":"https://preflight.art/v1/preflight/4c68c919-1ded-43b4-916e-f9e97ea4ce0e/status"}}}},"400":{"description":"File invalid or missing (e.g. wrong extension, malformed PDF header, password-protected, > 75 MB).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"File does not appear to be a valid PDF (magic bytes mismatch).","code":"INVALID_PDF"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/preflight/{id}/status":{"parameters":[{"$ref":"#/components/parameters/JobId"}],"get":{"tags":["Preflight"],"summary":"Check job status","description":"Lightweight poll endpoint. Poll this every 3–5 seconds until `status` is `complete` or `error`, then call GET /v1/preflight/{id}/report. Typical jobs finish in 60–120 seconds.\n\nThe bare GET /v1/preflight/{id} path returns the same payload and is retained as a deprecated alias for older integrations.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"Current job state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"},"example":{"job_id":"4c68c919-1ded-43b4-916e-f9e97ea4ce0e","status":"complete","profile_id":"flexo-labels","profile_name":"Flexo Labels","file_name":"label.pdf","started_at":"2026-05-15T20:52:09.221Z","completed_at":"2026-05-15T20:53:52.481Z","progress":"Analysis complete.","verdict":"CONDITIONAL_PASS","critical_count":1,"warning_count":7,"info_count":3}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/preflight/{id}":{"parameters":[{"$ref":"#/components/parameters/JobId"}],"get":{"tags":["Preflight"],"summary":"Check job status (deprecated alias)","description":"Deprecated alias of GET /v1/preflight/{id}/status — identical payload, retained for backward compatibility. New integrations should poll /status.","deprecated":true,"security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"Current job state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"},"example":{"job_id":"4c68c919-1ded-43b4-916e-f9e97ea4ce0e","status":"complete","profile_id":"flexo-labels","profile_name":"Flexo Labels","file_name":"label.pdf","started_at":"2026-05-15T20:52:09.221Z","completed_at":"2026-05-15T20:53:52.481Z","progress":"Analysis complete.","verdict":"CONDITIONAL_PASS","critical_count":1,"warning_count":7,"info_count":3}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Preflight"],"summary":"Delete a job and purge its artwork","description":"Unlinks the uploaded artwork from our storage immediately. The job row remains with `status: deleted` for audit retention, but the analysis blob, PDF report, and source file are cleared.","security":[{"ApiKeyAuth":["preflight:delete"]}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["deleted"]},"file_removed":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/preflight/{id}/report":{"parameters":[{"$ref":"#/components/parameters/JobId"}],"get":{"tags":["Preflight"],"summary":"Get the full analysis report","description":"Returns the complete structured analysis once the job is complete. Call only after GET /v1/preflight/{id} returns `status: complete`.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"Full report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobReport"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Job exists but is not yet complete. Keep polling GET /v1/preflight/{id}/status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Analysis is not yet complete. Poll GET /v1/preflight/:id/status for status.","code":"NOT_READY","status":"processing"}}}},"422":{"description":"Analysis failed for this file. The error message describes what went wrong.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"PDF appears to be corrupted or truncated. Re-export and resubmit.","code":"ANALYSIS_FAILED"}}}}}}},"/v1/preflight/{id}/pdf":{"parameters":[{"$ref":"#/components/parameters/JobId"}],"get":{"tags":["Preflight"],"summary":"Download the rendered PDF report","description":"Returns the human-readable PDF report (cover page, findings, technical summary, plate analysis). Same content the web app displays.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"PDF binary.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Job exists but the PDF is not yet available.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/preflight/{id}/source":{"parameters":[{"$ref":"#/components/parameters/JobId"}],"get":{"tags":["Preflight"],"summary":"Download the source artwork","description":"Re-downloads the original file you uploaded. The file is decrypted from at-rest storage and streamed back. Available until the job is deleted or 30 days, whichever comes first.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"Source file binary.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/webhooks/test":{"post":{"tags":["Webhooks"],"summary":"Send a test webhook to verify your endpoint","description":"Enqueues a `webhook.test` event for delivery. Use this during integration testing to confirm your endpoint receives the delivery and validates the `X-Preflight-Signature` header.\n\nThe target URL is the one configured on this API key (set at creation, or update via the account settings UI). Optionally override with the body field `url` — must be https://.","security":[{"ApiKeyAuth":["preflight:write"]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Optional override webhook URL. Must be https://."}}}}}},"responses":{"202":{"description":"Test event queued for delivery.","content":{"application/json":{"schema":{"type":"object","properties":{"event_id":{"type":"integer"},"target_url":{"type":"string","format":"uri"},"status":{"type":"string","enum":["queued"]},"poll_url":{"type":"string","format":"uri"}}}}}},"400":{"description":"Misconfiguration (no webhook URL on the key, override URL not https, or no webhook signing secret configured). Returns one of: `NO_WEBHOOK_URL`, `INVALID_WEBHOOK_URL`, `NO_WEBHOOK_SECRET`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/webhooks/events":{"get":{"tags":["Webhooks"],"summary":"List recent webhook delivery attempts for this key","description":"Returns the most recent webhook delivery attempts (success + retrying + abandoned) for the calling API key. Use `event_id` to fetch a single attempt — useful right after POST /v1/webhooks/test to confirm delivery.","security":[{"ApiKeyAuth":["preflight:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":50}},{"name":"event_id","in":"query","schema":{"type":"integer"},"description":"Fetch a single event by id."}],"responses":{"200":{"description":"Delivery history.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"event":{"$ref":"#/components/schemas/WebhookEvent","description":"Present only when ?event_id was provided."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"?event_id provided but no matching row for this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/preflight/{id}/links":{"parameters":[{"$ref":"#/components/parameters/JobId"}],"get":{"tags":["Preflight"],"summary":"Mint signed download links","description":"Links for the report JSON, the report PDF and the original upload that work with NO API key, for handing to a system that must not hold one (Zapier, Ziflow). They stop working at expires_at or when this key is revoked.","security":[{"ApiKeyAuth":["preflight:read"]}],"parameters":[{"name":"ttl_hours","in":"query","schema":{"type":"number","default":24,"maximum":168},"description":"Lifetime in hours; clamped to 168 (7 days). The response echoes the lifetime granted."}],"responses":{"200":{"description":"Links.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SignedLinks"},{"type":"object","properties":{"job_id":{"type":"string"},"expires_at":{"type":"string","format":"date-time"},"ttl_hours":{"type":"number"},"reference":{"type":"string"}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dl/{jobId}/{kind}":{"get":{"tags":["Preflight"],"summary":"Fetch a file by signed link","description":"Public: the link is the credential. Use the URLs from a webhook or GET /v1/preflight/{id}/links as delivered.","security":[],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}},{"name":"kind","in":"path","required":true,"schema":{"type":"string","enum":["pdf","source","report_json"]}},{"name":"k","in":"query","required":true,"schema":{"type":"string"}},{"name":"e","in":"query","required":true,"schema":{"type":"integer"}},{"name":"s","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The file (PDF, original, or report JSON)."},"403":{"description":"BAD_SIGNATURE or KEY_REVOKED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JOB_NOT_FOUND or FILE_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"LINK_EXPIRED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/intake-links":{"post":{"tags":["Intake links"],"summary":"Create an intake link for a quote","security":[{"ApiKeyAuth":["preflight:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reference","profile"],"properties":{"reference":{"type":"string","maxLength":128},"profile":{"type":"string","enum":["flexo-labels","digital-labels","flexpack","digital-flexpack","gravure-flexpack","offset-flexpack","cartons-folding","cartons-folding-digital"]},"depth":{"type":"string","enum":["quick","full"],"default":"quick"},"label":{"type":"string","maxLength":120},"customer_name":{"type":"string","maxLength":160},"job_description":{"type":"string","maxLength":4000},"metadata":{"type":"object","additionalProperties":{"type":["string","number","boolean"]}},"expires_in_days":{"type":"number","exclusiveMinimum":0,"maximum":3650,"description":"Omit for a link that does not expire."},"max_uploads":{"type":"integer","minimum":1,"maximum":100000,"description":"Omit for unlimited."}}}}}},"responses":{"201":{"description":"Created. `url` is shown this once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeLinkCreated"}}}},"400":{"description":"REFERENCE_REQUIRED, REFERENCE_TOO_LONG, INVALID_PROFILE, INVALID_DEPTH, INVALID_EXPIRY, INVALID_MAX_UPLOADS, INVALID_METADATA, METADATA_TOO_LARGE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"get":{"tags":["Intake links"],"summary":"List intake links","security":[{"ApiKeyAuth":["preflight:read"]}],"parameters":[{"name":"reference","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["active","revoked"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/IntakeLink"}},"next_cursor":{"type":["string","null"]}}}}}}}}},"/v1/intake-links/general":{"post":{"tags":["Intake links"],"summary":"The key's general upload page (no quote behind it)","description":"One general page per key, the same URL on every call: 201 the first time, then 200. The page asks the customer for their name, email and company (and optionally a quote or PO number) before it uploads; they come back on submission.ready as submitter_name, submitter_email, submitter_company and qre_hint, with link_kind general. The same file twice is counted per email address. After DELETE /v1/intake-links/{id}, the next call gives a new URL (reissued: true).","security":[{"ApiKeyAuth":["preflight:write"]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"profile":{"type":"string","description":"Empty or left out: files are received, not checked."},"depth":{"type":"string","enum":["quick","full"]},"label":{"type":"string"},"job_description":{"type":"string"}}}}}},"responses":{"200":{"description":"The same page as before (created: false), with any fields sent applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeLinkCreated"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeLinkCreated"}}}},"400":{"description":"INVALID_PROFILE, INVALID_DEPTH.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"INTAKE_TOKEN_SECRET_MISSING.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/intake-links/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Intake links"],"summary":"Get an intake link","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"The link (never the URL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeLink"}}}},"404":{"description":"INTAKE_LINK_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Intake links"],"summary":"Revoke an intake link","security":[{"ApiKeyAuth":["preflight:write"]}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeLink"}}}},"404":{"description":"INTAKE_LINK_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/intake-links/{id}/uploads":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Intake links"],"summary":"Uploads made on an intake link","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"link_id":{"type":"string"},"reference":{"type":"string"},"uploads":{"type":"array","items":{"$ref":"#/components/schemas/UploadBatch"}}}}}}}}}},"/v1/submissions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"The upload_id (iu_...) of an upload on an intake link, or of a multi-artwork zip.","schema":{"type":"string"}}],"get":{"tags":["Intake links"],"summary":"What submission.ready carries for this upload now, and its last send","description":"Builds the body submission.ready would carry right now (send_reason \"preview\") and sends nothing. `payload` is null while files are still being checked. Another key's submission is 404.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"The preview and the state of the last send.","content":{"application/json":{"schema":{"type":"object","properties":{"submission_id":{"type":"string"},"status":{"type":"string","enum":["processing","complete","partial","failed"]},"subscribed":{"type":"boolean","description":"Whether this key receives submission.ready (it must be named on the key's webhook events)."},"delivery":{"type":["object","null"],"properties":{"event_id":{"type":"string"},"status":{"type":"string","enum":["pending","success","abandoned","superseded"]},"attempt":{"type":"integer"},"max_attempts":{"type":"integer"},"last_response_code":{"type":["integer","null"]},"last_error":{"type":["string","null"]},"next_attempt_at":{"type":["string","null"],"format":"date-time"}}},"payload":{"oneOf":[{"$ref":"#/components/schemas/WebhookSubmissionReadyPayload"},{"type":"null"}]}}}}}},"404":{"description":"SUBMISSION_NOT_FOUND"}}}},"/v1/submissions/{id}/resend":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Intake links"],"summary":"Send submission.ready again","description":"A fresh body with the whole file list as it stands now, a new event_id and send_reason \"resend\". A send still waiting to retry is superseded, so only one send of a submission is ever in flight.","security":[{"ApiKeyAuth":["preflight:write"]}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"submission_id":{"type":"string"},"event_id":{"type":"string"},"superseded_event_id":{"type":["string","null"]},"status":{"type":"string","enum":["queued"]}}}}}},"400":{"description":"NO_WEBHOOK_URL"},"404":{"description":"SUBMISSION_NOT_FOUND"},"409":{"description":"SUBMISSION_NOT_READY, EVENT_NOT_SUBSCRIBED, or SEND_IN_PROGRESS (the last send is on the wire this moment; try again in a few seconds)."}}}},"/v1/submissions/{id}/files":{"post":{"tags":["Intake links"],"summary":"Add a corrected file to a submission","description":"The file is checked like the others (the link's profile and depth). When it is done, submission.ready goes out again with send_reason `corrected` and the files as they stand: a replaced file is left out and named in `replaced_file_ids`; the new one carries `replaces_file_id`. upload.completed is not sent again. One correction at a time per submission.","security":[{"ApiKeyAuth":["preflight:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The upload_id (iu_...)."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"replaces_file_id":{"type":"string","description":"The file_id it replaces. Leave out to add a file."}}}}}},"responses":{"202":{"description":"Accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"submission_id":{"type":"string"},"status":{"type":"string"},"file":{"type":"object","properties":{"file_id":{"type":"string"},"file_name":{"type":"string"},"kind":{"type":"string"},"replaces_file_id":{"type":["string","null"]},"status":{"type":"string"}}}}}}}},"400":{"description":"FILE_MISSING, TOO_MANY_FILES."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"SUBMISSION_NOT_FOUND, FILE_NOT_IN_SUBMISSION."},"409":{"description":"SUBMISSION_NOT_READY (being checked), FILE_ALREADY_REPLACED (with replaced_by), NOT_A_LINK_SUBMISSION."},"413":{"description":"FILE_TOO_LARGE."},"422":{"description":"ZIP_NOT_ACCEPTED, INVALID_FILE."}}}},"/v1/submissions/{id}/recheck":{"post":{"tags":["Intake links"],"summary":"Check a submission's files again under the link's print profile","description":"The artwork in the submission, as it stands, is checked again under the profile its link has now. When it is done, submission.ready goes out again with send_reason `rechecked`; each new file names the file it replaces and keeps its version. Calling POST /api/v1/upload-links (or /v1/intake-links) with the same quote_request_id and a different profile does this by itself for every finished submission on the link. Reference files are not checked. A file past its retention period is listed in `skipped`.","security":[{"ApiKeyAuth":["preflight:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The upload_id (iu_...)."}],"responses":{"202":{"description":"Started.","content":{"application/json":{"schema":{"type":"object","properties":{"submission_id":{"type":"string"},"status":{"type":"string"},"profile":{"type":"string"},"files":{"type":"array","items":{"type":"object","properties":{"file_id":{"type":"string"},"file_name":{"type":"string"},"replaces_file_id":{"type":"string"},"status":{"type":"string"}}}},"skipped":{"type":"array","items":{"type":"object"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"SUBMISSION_NOT_FOUND."},"409":{"description":"SUBMISSION_NOT_READY (still being checked), NOT_A_LINK_SUBMISSION, NO_PROFILE (the link has none), NOTHING_TO_RECHECK."}}}},"/v1/f/{token}":{"get":{"tags":["Intake links"],"summary":"A file link that never expires","description":"Public: the link is the credential. Redirects (302) to a fresh 15-minute signed download of the original file, named after the customer's own file. From `file_link` in submission.ready or GET /v1/files/{id}/links. Stops working when the links are turned off (410 LINK_REVOKED), the key is revoked (403 KEY_REVOKED), the job is deleted (404), or the file passes its retention period (410 FILE_EXPIRED).","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}],"responses":{"302":{"description":"To a 15-minute /v1/dl link."},"404":{"description":"LINK_NOT_FOUND or JOB_NOT_FOUND"},"410":{"description":"LINK_REVOKED or FILE_EXPIRED"}}}},"/v1/r/{token}":{"get":{"tags":["Intake links"],"summary":"A report link that never expires","description":"Public, like /v1/f/{token}, for the report PDF (`report_link` in submission.ready).","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}],"responses":{"302":{"description":"To a 15-minute /v1/dl link."},"404":{"description":"LINK_NOT_FOUND or JOB_NOT_FOUND"},"410":{"description":"LINK_REVOKED"}}}},"/v1/files/{id}/links":{"parameters":[{"name":"id","in":"path","required":true,"description":"The file id: `file_id` in submission.ready, the job id elsewhere.","schema":{"type":"string"}}],"get":{"tags":["Intake links"],"summary":"The never-expiring links to a file and its report","description":"The same URLs on every call, until they are turned off.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"The links.","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string"},"file_name":{"type":["string","null"]},"file_link":{"type":["string","null"]},"report_link":{"type":["string","null"]},"expires":{"type":"string","enum":["never"]}}}}}},"404":{"description":"FILE_NOT_FOUND"},"503":{"description":"FILE_LINKS_UNAVAILABLE"}}},"delete":{"tags":["Intake links"],"summary":"Turn a file's links off","description":"Every link to the file and its report answers 410 LINK_REVOKED from now on. The next GET gives new links.","security":[{"ApiKeyAuth":["preflight:write"]}],"responses":{"200":{"description":"{ file_id, revoked }"},"404":{"description":"FILE_NOT_FOUND"}}}},"/v1/files/{id}/ziflow-source":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Intake links"],"summary":"A 24-hour link to the file itself, for Ziflow","description":"For a package zip, its main artwork. Also served at GET /api/v1/files/{file_id}/ziflow-source with `Authorization: Bearer pf_...`.","security":[{"ApiKeyAuth":["preflight:read"]}],"responses":{"200":{"description":"The link.","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string"},"file_name":{"type":["string","null"]},"url":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}}}}}},"404":{"description":"FILE_NOT_FOUND"},"410":{"description":"FILE_EXPIRED"}}}},"/v1/intake/{token}/parts":{"post":{"tags":["Intake links"],"summary":"Start a file sent in pieces (resumable upload)","description":"Public: the token in the path is the credential. What the drop page does for anything over 8 MB. Then send the pieces, then hand the finished parts to POST /v1/intake/{token}/upload as `parts`. A part with no piece for a day is removed.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/PartStart"}},"application/json":{"schema":{"$ref":"#/components/schemas/PartStart"}}}},"responses":{"201":{"description":"Started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Part"}}}},"400":{"description":"FILE_NAME_REQUIRED, INVALID_SIZE."},"410":{"description":"INTAKE_LINK_REVOKED, INTAKE_LINK_EXPIRED, UPLOAD_LIMIT."},"413":{"description":"FILE_TOO_LARGE, SUBMISSION_TOO_LARGE (too much on its way up on this link at once)."},"422":{"description":"UNSUPPORTED_TYPE: a type the drop page would only skip. Nothing was sent."},"429":{"description":"TOO_MANY_PARTS, RATE_LIMITED."}}}},"/v1/intake/{token}/parts/{partId}":{"get":{"tags":["Intake links"],"summary":"How much of a file has arrived","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"partId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`received` is where the next piece starts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Part"}}}},"404":{"description":"PART_NOT_FOUND."}}},"post":{"tags":["Intake links"],"summary":"Send one piece","description":"The piece is the raw body (up to 16 MB), starting at byte `offset`, which must equal `received`. Sent with no content type, it needs no CORS preflight. A connection that drops keeps what arrived: ask for `received` and carry on.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"partId","in":"path","required":true,"schema":{"type":"string"}},{"name":"offset","in":"query","required":true,"schema":{"type":"integer","minimum":0}}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Part"}}}},"404":{"description":"PART_NOT_FOUND."},"409":{"description":"OFFSET_MISMATCH or PART_BUSY, with `received`."},"411":{"description":"LENGTH_REQUIRED."},"413":{"description":"PART_OVERFLOW (past the file's size) or CHUNK_TOO_LARGE (over 16 MB)."}}}},"/v1/intake/{token}/upload":{"post":{"tags":["Intake links"],"summary":"Upload to an intake link (what the drop page calls)","description":"Public: the token in the path is the credential. 1 to 25 files as multipart `files`, 2 GB in all; up to 25 artworks and 25 reference files after zips are opened. Reference files (.jpg .png .tif .psd .docx .xlsx) are kept and never checked (`kind: attachment`). A file this link already has is listed in `skipped` as `duplicate`; when every file was already here the answer is 200 with `status: duplicate` and nothing is stored, checked or sent.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"string","format":"binary"},"maxItems":25},"note":{"type":"string","maxLength":2000,"description":"Optional, any link: a note with the files."},"name":{"type":"string","description":"General page only, required."},"email":{"type":"string","format":"email","description":"General page only, required."},"company":{"type":"string","description":"General page only, required."},"qre_hint":{"type":"string","description":"General page only, optional: a quote or PO number the customer typed."},"email_results":{"type":"string","enum":["1","true","on"],"description":"Only where the key's brand offers it: email the customer the results when every file is done. Ignored otherwise."},"results_email":{"type":"string","format":"email","description":"Where to send them, on a quote page (the general page uses `email`). 400 SUBMITTER_REQUIRED or INVALID_EMAIL with field results_email."},"parts":{"type":"string","description":"Instead of (or with) files: the ids of finished parts, comma separated (POST /v1/intake/{token}/parts). All are taken, or none: 409 PART_INCOMPLETE names one not finished, 404 PART_NOT_FOUND one that is not this link's."}}}}}},"responses":{"200":{"description":"Every file was already received on this link: nothing new was stored, checked or reported, and the upload is not counted.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["duplicate"]},"upload_id":{"type":"null"},"file_count":{"type":"integer","enum":[0]},"message":{"type":"string"},"skipped":{"type":"array","items":{"type":"object"}}}}}}},"202":{"description":"Accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"upload_id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string"},"source_kind":{"type":"string"},"source_file_name":{"type":["string","null"]},"file_count":{"type":"integer"},"files":{"type":"array","items":{"type":"object","properties":{"job_id":{"type":"string"},"file_name":{"type":"string"},"status":{"type":"string"}}}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/SkippedEntry"}}}}}}},"400":{"description":"NO_ARTWORK, ENCRYPTED_ZIP, ZIP_TOO_LARGE, TOO_MANY_FILES, FILE_MISSING. On the general page also SUBMITTER_REQUIRED and INVALID_EMAIL, with `field` naming the box.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"INTAKE_LINK_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"INTAKE_LINK_REVOKED, INTAKE_LINK_EXPIRED, UPLOAD_LIMIT.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"FILE_TOO_LARGE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/intake/{token}/uploads/{uploadId}":{"get":{"tags":["Intake links"],"summary":"Poll an upload (what the drop page calls)","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The batch and a card per file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadBatch"}}}},"404":{"description":"UPLOAD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Required on every endpoint. Format: `pf_<32 url-safe characters>`. Contact support to be issued a key."}},"parameters":{"JobId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Job UUID returned by POST /v1/preflight."}},"responses":{"Unauthorized":{"description":"Missing, malformed, or unrecognised API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"API key not recognized.","code":"API_KEY_REQUIRED"}}}},"Forbidden":{"description":"Your account plan is not approved for v1 access, your key lacks the required scope, or the request came from outside the key's IP allowlist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Your account plan does not include v1 API access.","code":"V1_NOT_AVAILABLE_ON_THIS_PLAN"}}}},"NotFound":{"description":"No job is accessible with that id. Jobs that exist but belong to a different account return this same response, so a 404 does not confirm whether an id exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Job not found.","code":"JOB_NOT_FOUND"}}}},"RateLimited":{"description":"Hourly or daily request cap exceeded for this API key.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Hourly request cap exceeded for this API key.","code":"RATE_LIMIT_HOURLY"}}}}},"schemas":{"Profile":{"type":"object","properties":{"id":{"type":"string","example":"digital-labels"},"name":{"type":"string","example":"Digital Labels"},"description":{"type":"string"},"printMethod":{"type":"string","enum":["digital","flexo","flexpack","offset-litho"]}},"required":["id","name","printMethod"]},"JobAccepted":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing"]},"profile_id":{"type":"string"},"depth":{"type":"string","enum":["quick","full"]},"poll_url":{"type":"string","format":"uri"},"reference":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"upload_id":{"type":"string","description":"Only for a multi-artwork zip: the batch id reported by upload.completed."},"job_ids":{"type":"array","items":{"type":"string"},"description":"Only for a multi-artwork zip: one job per artwork, in order. job_id is the first."},"files":{"type":"array","items":{"type":"object","properties":{"job_id":{"type":"string"},"file_name":{"type":"string"},"status":{"type":"string"}}}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/SkippedEntry"}}},"required":["job_id","status","profile_id","poll_url"]},"Job":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing","complete","error","deleted"],"description":"`processing` — still running. `complete` — call /report. `error` — analysis failed. `deleted` — job was removed."},"profile_id":{"type":"string"},"profile_name":{"type":"string"},"file_name":{"type":"string"},"started_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"progress":{"type":"string","description":"Human-readable progress message — useful for showing the customer what step we're on."},"verdict":{"type":["string","null"],"enum":["PASS","CONDITIONAL_PASS","FAIL",null],"description":"Present once `status: complete`. PASS = press-ready as-is. CONDITIONAL_PASS = ready with caveats. FAIL = blocking issues."},"critical_count":{"type":["integer","null"],"minimum":0},"warning_count":{"type":["integer","null"],"minimum":0},"info_count":{"type":["integer","null"],"minimum":0},"error":{"type":"string","description":"Present only when `status: error`."}},"required":["job_id","status"]},"JobReport":{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["complete"]},"profile_id":{"type":"string"},"file_name":{"type":"string"},"started_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time"},"verdict":{"type":"string","enum":["PASS","CONDITIONAL_PASS","FAIL"]},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}},"passed_checks":{"type":"array","items":{"type":"string"},"description":"Human-readable list of checks the file passed cleanly."},"technical_summary":{"$ref":"#/components/schemas/TechnicalSummary"},"extracted_data":{"type":"object","description":"Full structured extraction output (geometry, color spaces, fonts, images, barcodes, dieline intelligence). Schema is broad and stable — see https://preflight.art/docs/api for field-level docs."},"counts":{"type":"object","properties":{"critical":{"type":"integer","minimum":0},"warning":{"type":"integer","minimum":0},"info":{"type":"integer","minimum":0}}},"report_pdf_url":{"type":"string","format":"uri","description":"Direct URL to GET /v1/preflight/{id}/pdf — include your `x-api-key` header."},"source_file_url":{"type":"string","format":"uri","description":"Direct URL to GET /v1/preflight/{id}/source."}},"required":["job_id","status","verdict","findings","counts","report_pdf_url"]},"Finding":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"severity":{"type":"string","enum":["critical","warning","info"]},"category":{"type":"string","description":"Domain category — e.g. color, dieline, geometry, image, barcode, font, structure."},"description":{"type":"string"},"fix":{"type":"string","description":"Recommended fix the prepress operator should apply."},"source":{"type":"string","enum":["extraction","claude","vision"],"description":"Which analysis layer surfaced this finding."}},"required":["title","severity"]},"TechnicalSummary":{"type":"object","description":"Mid-level summary of the artwork. Fields are nullable when the engine couldn't determine them with confidence.","properties":{"pageSize":{"type":["string","null"],"description":"Page or sheet dimensions in inches and mm."},"estimatedDieDimensions":{"type":["string","null"],"description":"Finished label / panel size derived from the dieline."},"bleed":{"type":["string","null"]},"dieline":{"type":["string","null"]},"dielineConfidence":{"type":["string","null"],"enum":["exact","high","medium","low","none",null]},"spotColors":{"type":["string","null"],"description":"Plate-level summary (spot color names + counts)."},"colorMode":{"type":["string","null"]},"maxTAC":{"type":["string","null"],"description":"Maximum Total Area Coverage observed."},"totalFonts":{"type":["integer","null"]},"missingFonts":{"type":["integer","null"]},"totalImages":{"type":["integer","null"]},"lowResImages":{"type":["integer","null"]},"whiteLayer":{"type":["string","null"],"description":"White underbase status (required / present / absent)."}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code. Documented values: API_KEY_REQUIRED, V1_NOT_AVAILABLE_ON_THIS_PLAN, IP_NOT_ALLOWED, API_KEY_REJECTION_RATE_LIMIT, RATE_LIMIT_HOURLY, RATE_LIMIT_DAILY, INVALID_PROFILE, INVALID_PDF, FILE_MISSING, PAYLOAD_TOO_LARGE, INSUFFICIENT_SCOPE, JOB_NOT_FOUND, NOT_READY, ANALYSIS_FAILED, NO_WEBHOOK_URL, NO_WEBHOOK_SECRET, INVALID_WEBHOOK_URL, EVENT_NOT_FOUND, INVALID_EVENT_ID, INTERNAL_ERROR."}},"required":["error"],"example":{"error":"API key not recognized.","code":"API_KEY_REQUIRED"}},"WebhookEvent":{"type":"object","description":"One row in the webhook delivery log. `status: pending` rows are scheduled for retry; `status: success` delivered cleanly; `status: abandoned` failed all 3 attempts.","properties":{"event_id":{"type":"integer","description":"Internal row id; pass to ?event_id= to look up later."},"event_type":{"type":"string","enum":["preflight.completed","preflight.failed","webhook.test"],"description":"preflight.completed — analysis succeeded; preflight.failed — analysis errored; webhook.test — issued via POST /v1/webhooks/test."},"job_id":{"type":["string","null"],"format":"uuid","description":"Null for webhook.test events."},"target_url":{"type":"string","format":"uri"},"status":{"type":"string","enum":["pending","success","abandoned"]},"attempt":{"type":"integer","minimum":0,"description":"Number of delivery attempts made so far (max 3)."},"last_response_code":{"type":["integer","null"]},"last_error":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}}},"WebhookCompletedPayload":{"type":"object","description":"Body POSTed to your webhook URL when a job completes successfully. JSON, signed with HMAC-SHA256 via the X-Preflight-Signature header. See https://preflight.art/v1/webhooks-guide for verification examples.","properties":{"event":{"type":"string","enum":["preflight.completed"]},"job_id":{"type":"string","format":"uuid"},"api_key_id":{"type":"string"},"verdict":{"type":"string","enum":["PASS","CONDITIONAL_PASS","FAIL"]},"counts":{"type":"object","properties":{"critical":{"type":"integer","minimum":0},"warning":{"type":"integer","minimum":0},"info":{"type":"integer","minimum":0}}},"report_url":{"type":"string","format":"uri","description":"Direct URL to GET /v1/preflight/{id}/report — call with your x-api-key header to fetch the full analysis."},"completed_at":{"type":"string","format":"date-time"},"depth":{"type":"string","enum":["quick","full"]},"file_name":{"type":["string","null"]},"profile_id":{"type":["string","null"]},"reference":{"type":["string","null"],"description":"The reference sent with the job, or the intake link's."},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"intake":{"type":["object","null"],"properties":{"link_id":{"type":["string","null"]},"upload_id":{"type":["string","null"]}}},"urls":{"oneOf":[{"$ref":"#/components/schemas/SignedLinks"},{"type":"null"}]},"urls_expire_at":{"type":["string","null"],"format":"date-time"}},"required":["event","job_id","api_key_id","verdict","completed_at"],"example":{"event":"preflight.completed","job_id":"4c68c919-1ded-43b4-916e-f9e97ea4ce0e","api_key_id":"apk_a2889a1af9f9a64e","verdict":"CONDITIONAL_PASS","counts":{"critical":1,"warning":6,"info":2},"report_url":"https://preflight.art/v1/preflight/4c68c919-1ded-43b4-916e-f9e97ea4ce0e/report","completed_at":"2026-05-15T22:29:04.162Z"}},"WebhookFailedPayload":{"type":"object","description":"Body POSTed to your webhook URL when a job fails during analysis. Same signing as WebhookCompletedPayload.","properties":{"event":{"type":"string","enum":["preflight.failed"]},"job_id":{"type":"string","format":"uuid"},"api_key_id":{"type":"string"},"error":{"type":"string","description":"Customer-friendly error message. Same format as Job.error on the polling endpoint."},"failed_at":{"type":"string","format":"date-time"},"depth":{"type":"string","enum":["quick","full"]},"file_name":{"type":["string","null"]},"profile_id":{"type":["string","null"]},"reference":{"type":["string","null"],"description":"The reference sent with the job, or the intake link's."},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"intake":{"type":["object","null"],"properties":{"link_id":{"type":["string","null"]},"upload_id":{"type":["string","null"]}}},"urls":{"oneOf":[{"$ref":"#/components/schemas/SignedLinks"},{"type":"null"}]},"urls_expire_at":{"type":["string","null"],"format":"date-time"}},"required":["event","job_id","api_key_id","error","failed_at"],"example":{"event":"preflight.failed","job_id":"8e3f1992-...","api_key_id":"apk_a2889a1af9f9a64e","error":"PDF appears to be corrupted or truncated. Re-export and resubmit.","failed_at":"2026-05-15T22:30:11.041Z"}},"WebhookTestPayload":{"type":"object","description":"Body POSTed when you call POST /v1/webhooks/test. Use this to verify your endpoint receives + verifies signed webhooks before going live.","properties":{"event":{"type":"string","enum":["webhook.test"]},"api_key_id":{"type":"string"},"sent_at":{"type":"string","format":"date-time"},"message":{"type":"string"}},"required":["event","api_key_id","sent_at"]},"SkippedEntry":{"type":"object","description":"Something in an upload that was not checked, and why.","properties":{"entry":{"type":"string","description":"File name; for a zip member, zipname/path/inside."},"reason":{"type":"string","enum":["nested_zip","unsupported_type","invalid_file","invalid_zip","encrypted_zip","zip_too_large","bundle_limit","package_extra"]},"message":{"type":"string","description":"A sentence you can pass to the customer."}}},"SignedLinks":{"type":"object","description":"Download links that work with no API key until expires_at, or until the key they were minted for is revoked.","properties":{"report_json":{"type":"string","format":"uri"},"pdf":{"type":["string","null"],"format":"uri","description":"null when no report PDF was rendered."},"source":{"type":["string","null"],"format":"uri","description":"The original upload; null once it has passed its retention."}}},"UploadFile":{"type":"object","properties":{"job_id":{"type":"string"},"kind":{"type":"string","enum":["artwork","attachment"],"description":"attachment: a reference file kept with the upload and never checked (result Received)."},"file_name":{"type":"string"},"status":{"type":"string","enum":["processing","complete","failed"]},"depth":{"type":"string","enum":["quick","full"]},"verdict":{"type":["string","null"]},"quick_verdict":{"type":["string","null"]},"counts":{"type":["object","null"],"properties":{"critical":{"type":"integer"},"warning":{"type":"integer"},"info":{"type":"integer"}}},"error":{"type":"string","description":"Only when status is failed."},"urls":{"oneOf":[{"$ref":"#/components/schemas/SignedLinks"},{"type":"null"}]},"urls_expire_at":{"type":["string","null"],"format":"date-time"}}},"WebhookSubmissionReadyPayload":{"type":"object","description":"submission.ready: sent once per upload when every file has finished, ONLY to a key whose webhook events name it. Flat: every top-level value except `files` is a string (counts too), and a value we do not have is an empty string. Retried after 1, 5 and 30 minutes, then hourly for 24 hours; every retry carries the same body and event_id. A resend is a new event_id.","properties":{"event":{"type":"string","enum":["submission.ready"]},"event_id":{"type":"string","description":"The delivery's own id, the same as the X-Preflight-Event-Id header."},"send_reason":{"type":"string","enum":["completed","resend","corrected","rechecked","test","preview"],"description":"completed: the first send. resend: sent again on request. corrected: a corrected file was added. rechecked: the files were checked again under the link's new print profile. test: a sample from POST /v1/webhooks/test."},"replaced_file_ids":{"type":"string","description":"Files replaced by corrected ones, joined with \\n. They are not in `files`."},"corrected_count":{"type":"string","description":"How many files in `files` replaced another."},"test":{"type":"string","enum":["true","false"]},"submission_id":{"type":"string"},"upload_id":{"type":"string"},"intake_link_id":{"type":"string"},"link_kind":{"type":"string","enum":["quote","general",""],"description":"general: the key's general upload page. Empty for an API zip."},"submitter_name":{"type":"string","description":"General page: the name the customer typed."},"submitter_email":{"type":"string"},"submitter_company":{"type":"string"},"qre_hint":{"type":"string","description":"General page: a quote or PO number the customer typed, unchecked."},"note":{"type":"string","description":"Any page: the note the customer wrote with the files."},"quote_request_id":{"type":"string"},"qre":{"type":"string"},"project_name":{"type":"string"},"company":{"type":"string"},"customer_name":{"type":"string"},"rep_email":{"type":"string"},"rep_name":{"type":"string"},"reference":{"type":"string"},"profile":{"type":"string","description":"Empty when the link has no profile (files received, not checked)."},"depth":{"type":"string"},"result":{"type":"string","enum":["ready","ready_with_notes","needs_changes","could_not_read","received"],"description":"The worst result of the files. ready: checked, nothing to change. ready_with_notes: checked, no critical; warnings or notes to read. needs_changes: checked, at least one critical to fix (the quick check and the full report alike). could_not_read: the file could not be opened or checked. received: kept, not checked (a reference file, or a link with no profile)."},"result_label":{"type":"string","enum":["Ready","Ready with notes","Needs changes","Could not read","Received"]},"upload_status":{"type":"string","enum":["complete","partial","failed"],"description":"partial: some files could not be checked. failed: none could; error_code says why."},"file_count":{"type":"string"},"artwork_count":{"type":"string"},"attachment_count":{"type":"string","description":"Reference files kept with the upload, never checked."},"ready_count":{"type":"string"},"ready_with_notes_count":{"type":"string"},"needs_changes_count":{"type":"string"},"could_not_read_count":{"type":"string"},"received_count":{"type":"string"},"critical_count":{"type":"string"},"warning_count":{"type":"string"},"info_count":{"type":"string"},"findings":{"type":"string","description":"Up to 8 lines joined with \\n, criticals first then warnings, each \"SEVERITY: title\" and at most 150 characters. With more than one file each line ends with the file name in brackets."},"findings_total":{"type":"string"},"file_names":{"type":"string","description":"Joined with \\n."},"report_pdf_urls":{"type":"string","description":"Signed links, joined with \\n. They last 7 days."},"original_file_urls":{"type":"string"},"links_expire_at":{"type":"string"},"file_links":{"type":"string","description":"Links to the original files that never expire, joined with \\n."},"report_links":{"type":"string","description":"Links to the report PDFs that never expire, joined with \\n."},"skipped_count":{"type":"string"},"skipped":{"type":"string"},"error_code":{"type":"string","description":"When upload_status is failed: ENCRYPTED_ZIP, ZIP_TOO_LARGE, NO_ARTWORK, ALL_FILES_FAILED or UPLOAD_FAILED."},"error_message":{"type":"string"},"submitted_at":{"type":"string"},"completed_at":{"type":"string"},"generated_at":{"type":"string"},"files":{"type":"array","description":"Every file, every send. Each value a string.","items":{"type":"object","properties":{"file_id":{"type":"string"},"file_name":{"type":"string"},"kind":{"type":"string","enum":["artwork","attachment"]},"result":{"type":"string","enum":["ready","ready_with_notes","needs_changes","could_not_read","received"],"description":"This file's result code, the same codes as the top-level result. Route on this."},"result_label":{"type":"string","enum":["Ready","Ready with notes","Needs changes","Could not read","Received"]},"verdict":{"type":"string","description":"The verdict as the PDF report states it: PASS, CONDITIONAL_PASS or FAIL; NOT_CHECKED for a file that was not checked; empty when the check itself failed. A file that could not be read also shows FAIL: result tells the two apart. A full report keeps FAIL for a file that cannot be used at all, so a file with a critical to fix is CONDITIONAL_PASS here and needs_changes in result."},"critical_count":{"type":"string","description":"Every critical on this file, including total ink coverage over the limit, which is reported but does not hold a file on its own. Empty when the file was not checked."},"warning_count":{"type":"string","description":"Empty when the file was not checked."},"info_count":{"type":"string"},"findings":{"type":"string"},"findings_total":{"type":"string"},"report_pdf_url":{"type":"string"},"report_json_url":{"type":"string"},"original_file_url":{"type":"string"},"links_expire_at":{"type":"string"},"file_link":{"type":"string","description":"Never expires: /v1/f/{token}."},"report_link":{"type":"string","description":"Never expires: /v1/r/{token}."},"replaces_file_id":{"type":"string","description":"A corrected file: the file_id it replaced. Empty otherwise."},"version":{"type":"string","description":"Which version of the artwork this file is: \"1\" as first sent, \"2\" after one corrected file, and so on."},"rechecked":{"type":"string","enum":["true","false"],"description":"true: the same artwork checked again under the link's new print profile; replaces_file_id names the earlier check. The version does not change."},"error":{"type":"string"}}}}},"additionalProperties":{"type":"string","description":"metadata_<key>: the link's metadata, flat."}},"WebhookUploadCompletedPayload":{"type":"object","description":"Sent ONCE per upload (drop page, or a multi-artwork zip on POST /v1/preflight), after every file's own event. event is upload.failed when nothing could be checked; it then carries error.","properties":{"event":{"type":"string","enum":["upload.completed","upload.failed"]},"upload_id":{"type":"string"},"intake_link_id":{"type":["string","null"]},"api_key_id":{"type":"string"},"reference":{"type":["string","null"]},"metadata":{"type":["object","null"]},"customer_name":{"type":["string","null"]},"profile_id":{"type":["string","null"]},"depth":{"type":["string","null"]},"source_kind":{"type":"string","enum":["files","zip"]},"source_file_name":{"type":["string","null"]},"file_count":{"type":"integer"},"status":{"type":"string","enum":["complete","partial","failed"]},"verdict":{"type":["string","null"],"description":"Worst across the files: UNREADABLE, then FAIL / FIX_REQUIRED, then CONDITIONAL_PASS, then PASS."},"files":{"type":"array","items":{"$ref":"#/components/schemas/UploadFile"}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/SkippedEntry"}},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"uploaded_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time"},"note":{"type":"string","description":"Only when the customer wrote one."},"submitter":{"type":"object","description":"Only from the general upload page.","properties":{"name":{"type":"string"},"email":{"type":"string"},"company":{"type":"string"},"qre_hint":{"type":["string","null"]}}}}},"IntakeLink":{"type":"object","properties":{"id":{"type":"string"},"recheck":{"type":"object","description":"Only when this call changed the print profile of a quote link: the finished submissions now being checked again under it (each sends submission.ready again with send_reason rechecked), and any left alone because they were still being checked.","properties":{"from":{"type":"string"},"to":{"type":"string"},"submissions":{"type":"array","items":{"type":"object","properties":{"submission_id":{"type":"string"},"files":{"type":"integer"}}}},"skipped":{"type":"array","items":{"type":"object","properties":{"submission_id":{"type":"string"},"code":{"type":"string"},"error":{"type":"string"}}}}}},"kind":{"type":"string","enum":["quote","general"],"description":"general: the key's one general upload page (POST /v1/intake-links/general)."},"reference":{"type":"string"},"profile_id":{"type":"string"},"depth":{"type":"string","enum":["quick","full"]},"label":{"type":["string","null"]},"customer_name":{"type":["string","null"]},"job_description":{"type":["string","null"]},"metadata":{"type":["object","null"]},"status":{"type":"string","enum":["active","revoked"]},"state":{"type":"string","enum":["active","revoked","expired","upload_limit"]},"token_last4":{"type":"string"},"uploads_count":{"type":"integer"},"max_uploads":{"type":["integer","null"]},"expires_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"},"profile_held":{"type":"boolean","description":"The profile's press-specific checks are still being added."}}},"PartStart":{"type":"object","required":["file_name","size"],"properties":{"file_name":{"type":"string"},"size":{"type":"integer","description":"The whole file's size in bytes."}}},"Part":{"type":"object","properties":{"part_id":{"type":"string"},"file_name":{"type":"string"},"size":{"type":"integer"},"received":{"type":"integer","description":"Bytes stored so far: where the next piece starts."},"complete":{"type":"boolean"},"chunk_bytes":{"type":"integer","description":"The piece size to send (8 MB)."}}},"IntakeLinkCreated":{"allOf":[{"$ref":"#/components/schemas/IntakeLink"},{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The drop page. Returned ONCE; only a hash is stored."}}}]},"UploadBatch":{"type":"object","properties":{"upload_id":{"type":"string"},"intake_link_id":{"type":["string","null"]},"reference":{"type":["string","null"]},"source_kind":{"type":"string"},"source_file_name":{"type":["string","null"]},"file_count":{"type":"integer"},"completed_count":{"type":"integer"},"failed_count":{"type":"integer"},"status":{"type":"string","enum":["processing","complete","partial","failed"]},"job_ids":{"type":"array","items":{"type":"string"}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/SkippedEntry"}},"uploaded_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}}}},"headers":{"XPreflightKeyExpiresAt":{"description":"Present on every response made with an API key that is in its rotation grace window (i.e. a new key was issued via /account/api-keys/:id/rotate and the old key has a scheduled revoked_at in the future). Watch for this header in your monitoring so you swap your integration over to the new key before the deadline.","schema":{"type":"string","format":"date-time"}},"RetryAfter":{"description":"Returned on 429 responses. Seconds the client should wait before retrying.","schema":{"type":"integer","minimum":1}}}}}