{"components":{"responses":{"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Resource not found"},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Not authenticated (or session expired)"}},"schemas":{"AutoAction":{"description":"One auto_actions audit row (#52) — an automated action the system took or proposed, with its payload evidence, qualitative confidence tier, and confirmation/reversal stamps. Append-only; rows only ever gain stamps.","properties":{"action_type":{"description":"Machine token — document_routed, payment_proposed, ...","type":"string"},"confidence":{"enum":["high","medium","low",null],"type":["string","null"]},"confirmed_at":{"type":["string","null"]},"created_at":{"type":"string"},"id":{"format":"uuid","type":"string"},"payload_json":{"description":"The JSONB payload as a JSON-encoded string.","type":"string"},"requires_confirmation":{"type":"boolean"},"reversed_at":{"type":["string","null"]},"reversible":{"description":"Server verdict on whether /api/audit/reverse can act.","type":"boolean"},"subject_id":{"format":"uuid","type":"string"},"subject_table":{"type":"string"}},"required":["id","action_type","subject_table","subject_id","payload_json","requires_confirmation","created_at","reversible"],"type":"object"},"CaptureLinkSummary":{"description":"Sender-side view of a QR capture link. capture_path is the relative public path (/c/<token>) to encode as the QR payload.","properties":{"capture_path":{"type":"string"},"created_at":{"type":"string"},"expires_at":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"job_id":{"format":"uuid","type":"string"},"label":{"type":"string"},"revoked_at":{"type":["string","null"]}},"required":["id","job_id","label","capture_path","created_at"],"type":"object"},"CapturePresignedUpload":{"description":"Presigned R2 PUT URL pair for one photo upload.","properties":{"expires_in_seconds":{"type":"integer"},"r2_key":{"type":"string"},"r2_thumbnail_key":{"type":"string"},"thumbnail_upload_url":{"type":"string"},"upload_url":{"type":"string"}},"required":["r2_key","r2_thumbnail_key","upload_url","thumbnail_upload_url","expires_in_seconds"],"type":"object"},"CaptureRegisteredPhoto":{"description":"photo_assets row created by a register call.","properties":{"capture_run_id":{"format":"uuid","type":["string","null"]},"captured_at":{"type":"string"},"client_id":{"type":"string"},"id":{"format":"uuid","type":"string"},"job_id":{"format":"uuid","type":"string"},"original_sha256":{"type":["string","null"]},"r2_key":{"type":["string","null"]},"r2_thumbnail_key":{"type":["string","null"]},"upload_status":{"type":"string"}},"required":["id","job_id","client_id","upload_status","captured_at"],"type":"object"},"CaptureRunSummary":{"description":"One on-site photo-capture session against a job.","properties":{"completed_at":{"type":["string","null"]},"completion_score":{"type":["number","null"]},"id":{"format":"uuid","type":"string"},"job_id":{"format":"uuid","type":"string"},"started_at":{"type":"string"},"status":{"enum":["in_progress","completed","abandoned","pending_review"],"type":"string"},"template_id":{"format":"uuid","type":"string"},"template_version":{"type":"integer"}},"required":["id","job_id","template_id","status","template_version","started_at"],"type":"object"},"DocumentDetail":{"properties":{"document":{"$ref":"#/components/schemas/DocumentSummary"},"extractions":{"items":{"$ref":"#/components/schemas/DocumentExtractionView"},"type":"array"}},"required":["document","extractions"],"type":"object"},"DocumentExtractionView":{"properties":{"created_at":{"type":"string"},"engine":{"type":"string"},"fields_json":{"description":"Structured extraction fields as a JSON-encoded string.","type":"string"},"id":{"format":"uuid","type":"string"},"raw_text":{"type":["string","null"]}},"required":["id","engine","fields_json","created_at"],"type":"object"},"DocumentPresignResponse":{"properties":{"expires_in_seconds":{"type":"integer"},"r2_key":{"type":"string"},"upload_url":{"type":"string"}},"required":["r2_key","upload_url","expires_in_seconds"],"type":"object"},"DocumentSummary":{"properties":{"client_id":{"type":"string"},"created_at":{"type":"string"},"customer_id":{"format":"uuid","type":["string","null"]},"document_kind":{"type":["string","null"]},"file_size_bytes":{"type":["integer","null"]},"id":{"format":"uuid","type":"string"},"job_id":{"format":"uuid","type":["string","null"]},"mime_type":{"type":"string"},"original_filename":{"type":["string","null"]},"page_count":{"type":["integer","null"]},"routed_at":{"type":["string","null"]},"status":{"enum":["uploaded","processing","extracted","routed","needs_review","failed"],"type":"string"}},"required":["id","client_id","status","mime_type","created_at"],"type":"object"},"ExportBundle":{"description":"Complete machine-readable export of everything the signed-in user owns (#29 data-freedom export). Each section is an array of plain records whose exact field set is defined by the Rust wire types in apps/web-leptos/src/server_fns/export.rs (schema_version \"1\"); consumers should treat unknown fields as forward-compatible additions. Photo/PDF bytes are NOT inlined — photo originals come via /api/export/photo_urls.","properties":{"canvas_documents":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"capture_runs":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"crm":{"additionalProperties":true,"description":"Team-scoped CRM section (leads, customers, proposals, activities, tasks, renewals, payments, signature_requests). Null when the caller has no active team.","type":["object","null"]},"document_extractions":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"document_requests":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"documents":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"jobs":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"manifest":{"$ref":"#/components/schemas/ExportManifest"},"photo_assets":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"photo_packets":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"profile":{"additionalProperties":true,"type":"object"},"saved_area_alerts":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"saved_areas":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"scanned_documents":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"storm_alert_subscriptions":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"storm_reports":{"items":{"additionalProperties":true,"type":"object"},"type":"array"}},"required":["manifest","profile","jobs","capture_runs","photo_assets","documents","document_extractions","document_requests","storm_reports","photo_packets","canvas_documents","scanned_documents","storm_alert_subscriptions","saved_areas","saved_area_alerts"],"type":"object"},"ExportManifest":{"description":"Completeness contract for the data-freedom export bundle — per-entity record counts + generation timestamp + bundle schema version. Wire shape of apps/web-leptos server_fns/export.rs ExportManifest.","properties":{"counts":{"additionalProperties":{"minimum":0,"type":"integer"},"description":"Per-entity record counts; keys match bundle sections.","type":"object"},"generated_at":{"format":"date-time","type":"string"},"schema_version":{"enum":["1"],"type":"string"},"team_id":{"format":"uuid","type":["string","null"]},"user_id":{"format":"uuid","type":"string"}},"required":["schema_version","generated_at","user_id","counts"],"type":"object"},"ExportPhotoUrl":{"description":"One presigned photo-original download entry.","properties":{"capture_run_id":{"format":"uuid","type":["string","null"]},"captured_at":{"format":"date-time","type":"string"},"download_url":{"description":"Presigned R2 GET URL for the original bytes (1h TTL).","type":"string"},"file_size_bytes":{"type":["integer","null"]},"job_id":{"format":"uuid","type":"string"},"mime_type":{"type":["string","null"]},"photo_id":{"format":"uuid","type":"string"},"r2_key":{"type":"string"},"thumbnail_download_url":{"description":"Presigned GET URL for the thumbnail, when registered.","type":["string","null"]}},"required":["photo_id","job_id","r2_key","captured_at","download_url"],"type":"object"},"ExportPhotoUrlsResponse":{"description":"One page of presigned photo download URLs. returned < limit means last page.","properties":{"expires_in_seconds":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"returned":{"type":"integer"},"urls":{"items":{"$ref":"#/components/schemas/ExportPhotoUrl"},"type":"array"}},"required":["urls","limit","offset","returned","expires_in_seconds"],"type":"object"},"GenerateStormReportOutput":{"description":"Wire shape of apps/web-leptos server_fns/storm_reports.rs GenerateStormReportOutput. FINAL GATE (2026-06-12): pdf_base64 is OMITTED by the public generate endpoint — the artifact releases through download_url after the autonomous quality review stamps green (poll /api/storm_reports/review_status).","properties":{"credits_remaining":{"description":"Caller's credit balance after the 1-credit deduction.","type":"integer"},"download_url":{"description":"Gated download path (/api/storm_reports/{purchase_id}/download) — releases 200 only after the review stamps green; 202 while pending, 409 when held.","type":"string"},"page_count":{"description":"Page count reported by pdf-engine.","minimum":0,"type":"integer"},"pdf_base64":{"description":"Base64-encoded PDF bytes. Absent from the public generate endpoint since the FINAL GATE (the field survives for server-internal compile callers).","type":"string"},"purchase_id":{"description":"Persisted storm_report_purchases row id — the report's identity in /account/storm-reports.","format":"uuid","type":"string"},"report_id":{"description":"\"STR-<uuid>\" report id printed on the PDF cover and persisted on the purchase row.","type":"string"},"review_status":{"description":"FINAL-GATE review state at response time — always \"pending\" from the public generate endpoint (the review job was just enqueued). Empty string only from server-internal callers.","enum":["pending","green","red","error","released_manual",""],"type":"string"},"size_bytes":{"description":"Decoded byte length (size-check before decode).","format":"int64","minimum":0,"type":"integer"}},"required":["page_count","size_bytes"],"type":"object"},"HealthResponse":{"properties":{"build":{"properties":{"codegen_units":{"type":"integer"},"commit":{"maxLength":40,"minLength":7,"type":"string"},"lto":{"type":"string"},"profile":{"enum":["release","debug","dev"],"type":"string"},"rustc":{"type":"string"},"timestamp_epoch":{"format":"int64","type":"integer"}},"required":["profile","commit","lto","codegen_units"],"type":"object"},"checks":{"additionalProperties":{"properties":{"latencyMs":{"type":"number"},"status":{"type":"string"}},"type":"object"},"type":"object"},"metrics":{"properties":{"errorRate":{"type":"number"},"errorsTotal":{"type":"integer"},"healthy":{"type":"boolean"},"issues":{"items":{"type":"string"},"type":"array"},"requestsTotal":{"type":"integer"},"uptime":{"type":"integer"}},"type":"object"},"status":{"enum":["healthy","degraded","unhealthy"],"type":"string"},"version":{"type":"string"}},"required":["status","version","build","checks","metrics"],"type":"object"},"JobPaymentsResponse":{"properties":{"confirmed_total_cents":{"description":"Sum of CONFIRMED payments only.","type":"integer"},"payments":{"items":{"$ref":"#/components/schemas/Payment"},"type":"array"}},"required":["payments","confirmed_total_cents"],"type":"object"},"LoginRequest":{"properties":{"email":{"format":"email","type":"string"},"password":{"minLength":8,"type":"string"}},"required":["email","password"],"type":"object"},"LoginResponse":{"properties":{"accessToken":{"description":"Present only when X-Client-Platform=ios (Bearer reuse).","type":"string"},"user":{"$ref":"#/components/schemas/SessionUser"}},"required":["user"],"type":"object"},"MaterialOrder":{"description":"One persisted material order (a `material_orders` row). Lifecycle\n`draft` → `finalized`; items/totals/assumptions are owned by the\ncalculators kernel (totals always server-recomputed).\n","properties":{"assumptions":{"items":{"type":"string"},"type":"array"},"created_at":{"format":"date-time","type":"string"},"finalized_at":{"format":"date-time","nullable":true,"type":"string"},"has_pdf":{"description":"True once the order-sheet PDF landed in R2.","type":"boolean"},"id":{"format":"uuid","type":"string"},"items":{"items":{"$ref":"#/components/schemas/MaterialOrderItem"},"type":"array"},"job_id":{"format":"uuid","type":"string"},"label":{"type":"string"},"roof_source":{"description":"Measurement provenance, e.g. `lidar:building_roofs/<uuid>`.","nullable":true,"type":"string"},"status":{"enum":["draft","finalized"],"type":"string"},"totals":{"$ref":"#/components/schemas/MaterialOrderTotals"},"updated_at":{"format":"date-time","type":"string"},"waste_factor_pct":{"maximum":30,"minimum":0,"type":"integer"}},"required":["id","job_id","status","label","items","totals","assumptions","waste_factor_pct","has_pdf","created_at","updated_at"],"type":"object"},"MaterialOrderItem":{"description":"One editable line item. `quantity` is numeric so review UIs can tweak\nit in place; `basis` records the formula that produced the starting\nvalue so a tweaked row stays auditable.\n","properties":{"basis":{"type":"string"},"code_required":{"description":"True when the item satisfies a building-code requirement.","type":"boolean"},"description":{"type":"string"},"note":{"description":"Short note carried onto the PDF row (e.g. \"27.5 squares incl. 10% waste\").","nullable":true,"type":"string"},"quantity":{"format":"double","type":"number"},"sku_hint":{"description":"Generic SKU hint (SHNGL-ARCH, UL-SYN, IWS, DRIP-EDGE, RIDGE-CAP, STARTER).","type":"string"},"unit":{"enum":["bundles","squares","LF"],"type":"string"}},"required":["sku_hint","description","quantity","unit","basis","code_required"],"type":"object"},"MaterialOrderPdfDownload":{"properties":{"download_url":{"description":"Presigned R2 GET URL (Content-Disposition attachment).","format":"uri","type":"string"},"expires_in_seconds":{"type":"integer"},"file_name":{"description":"Suggested filename, e.g. material-order-main-house-re-roof-0193e9c2.pdf.","type":"string"}},"required":["download_url","expires_in_seconds","file_name"],"type":"object"},"MaterialOrderPreview":{"properties":{"assumptions":{"description":"Derivation assumptions the reviewer should confirm.","items":{"type":"string"},"type":"array"},"items":{"items":{"$ref":"#/components/schemas/MaterialOrderItem"},"type":"array"},"job_id":{"format":"uuid","type":"string"},"roof":{"$ref":"#/components/schemas/MaterialOrderRoofSummary"},"roof_source":{"description":"Measurement provenance, e.g. `lidar:building_roofs/<uuid>`.","type":"string"},"totals":{"$ref":"#/components/schemas/MaterialOrderTotals"},"waste_factor_pct":{"maximum":30,"minimum":0,"type":"integer"}},"required":["job_id","items","totals","assumptions","roof_source","waste_factor_pct","roof"],"type":"object"},"MaterialOrderPreviewNoRoof":{"description":"Typed coverage state — the job exists but no roof model can be\nlocated (address not geocoded yet, or outside LiDAR coverage).\nReturned with HTTP 200; render an explainer, not an error toast.\n","properties":{"reason":{"type":"string"},"status":{"const":"no_roof","type":"string"}},"required":["status","reason"],"type":"object"},"MaterialOrderPreviewOk":{"properties":{"preview":{"$ref":"#/components/schemas/MaterialOrderPreview"},"status":{"const":"ok","type":"string"}},"required":["status","preview"],"type":"object"},"MaterialOrderRoofSummary":{"properties":{"eaves_ft":{"format":"double","type":"number"},"facet_count":{"type":"integer"},"hips_ft":{"format":"double","type":"number"},"lidar_quality_level":{"description":"LiDAR quality level of the source extraction (e.g. QL1).","type":"string"},"predominant_pitch":{"description":"Trade-standard rise/12 label, e.g. \"4/12\"; \"unknown\" when unmeasured.","type":"string"},"rakes_ft":{"format":"double","type":"number"},"ridges_ft":{"format":"double","type":"number"},"roof_id":{"format":"uuid","type":"string"},"total_roof_area_sqft":{"format":"double","type":"number"},"total_squares":{"format":"double","type":"number"},"valleys_ft":{"format":"double","type":"number"}},"required":["roof_id","total_roof_area_sqft","total_squares","predominant_pitch","facet_count","ridges_ft","hips_ft","valleys_ft","eaves_ft","rakes_ft"],"type":"object"},"MaterialOrderSaved":{"properties":{"order":{"$ref":"#/components/schemas/MaterialOrder"},"status":{"const":"saved","type":"string"}},"required":["status","order"],"type":"object"},"MaterialOrderTotals":{"properties":{"item_count":{"minimum":0,"type":"integer"},"shingle_bundles":{"minimum":0,"type":"integer"},"squares_with_waste":{"format":"double","type":"number"},"total_linear_ft":{"format":"double","type":"number"}},"required":["item_count","shingle_bundles","squares_with_waste","total_linear_ft"],"type":"object"},"Payment":{"description":"One job_payments ledger row (#52, decision #14). Integer cents. status moves proposed -> confirmed only via /api/payments/confirm (an explicit human action); proposed -> voided dismisses. job_street_address rides along on listing surfaces that JOIN it (proposed inbox, customer ledger) for the confirmation prompt.","properties":{"amount_cents":{"type":"integer"},"confirmed_at":{"type":["string","null"]},"created_at":{"type":"string"},"currency":{"type":"string"},"customer_id":{"format":"uuid","type":["string","null"]},"document_id":{"format":"uuid","type":["string","null"]},"id":{"format":"uuid","type":"string"},"job_id":{"format":"uuid","type":"string"},"job_street_address":{"type":["string","null"]},"method":{"enum":["check","ach","wire","card","cash","other"],"type":"string"},"notes":{"type":["string","null"]},"proposed_by":{"enum":["system","user"],"type":"string"},"received_at":{"format":"date","type":["string","null"]},"reference":{"type":["string","null"]},"status":{"enum":["proposed","confirmed","voided"],"type":"string"},"voided_at":{"type":["string","null"]}},"required":["id","job_id","amount_cents","currency","method","status","proposed_by","created_at"],"type":"object"},"PermitRow":{"properties":{"address":{"type":"string"},"id":{"format":"uuid","type":"string"},"jurisdictionName":{"type":"string"},"lookupAt":{"format":"date-time","type":"string"}},"required":["id","address","lookupAt"],"type":"object"},"PhotoPacketRow":{"properties":{"createdAt":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"label":{"type":"string"},"photoCount":{"type":"integer"},"shareUrl":{"description":"Public share URL (token-gated).","format":"uri","type":"string"}},"required":["id","label","createdAt"],"type":"object"},"ProblemDetails":{"description":"RFC 9457 (was 7807) problem-details payload.","properties":{"detail":{"type":"string"},"instance":{"type":"string"},"status":{"type":"integer"},"title":{"type":"string"},"type":{"format":"uri","type":"string"}},"type":"object"},"ProposalSummaryWithLead":{"description":"CRM proposal JOIN-enriched with lead business name + location. Wire shape of apps/web-leptos server_fns/crm/proposals.rs ProposalSummaryWithLead (snake_case, timestamps RFC-3339 strings).","properties":{"business_name":{"type":"string"},"city":{"type":["string","null"]},"created_at":{"type":"string"},"expires_at":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"lead_id":{"format":"uuid","type":"string"},"proposal_number":{"type":"string"},"sent_at":{"type":["string","null"]},"state_abbr":{"type":["string","null"]},"status":{"type":"string"},"value_cents":{"format":"int64","type":"integer"},"view_count":{"type":"integer"}},"required":["id","lead_id","proposal_number","value_cents","status","view_count","created_at","business_name"],"type":"object"},"PublicCaptureView":{"description":"What a capture token resolves to for the installer-facing screen. state=ok carries display labels only; not_found covers revoked, expired, and never-existed uniformly.","properties":{"address_line":{"type":["string","null"]},"customer_name":{"type":["string","null"]},"label":{"type":["string","null"]},"state":{"enum":["ok","not_found"],"type":"string"}},"required":["state"],"type":"object"},"PublicSignResult":{"description":"Outcome of a public sign/decline/consent submission.","properties":{"consented_at":{"type":["string","null"]},"declined_at":{"type":["string","null"]},"signed_at":{"type":["string","null"]},"state":{"enum":["signed","declined","consented","not_found","voided","expired","already_signed","already_declined"],"type":"string"}},"required":["state"],"type":"object"},"PublicSignView":{"description":"What a sign token resolves to. state=ok carries the document snapshot; terminal states (not_found/voided/expired) carry nothing.","properties":{"consented_at":{"description":"When ESIGN §101(c) consent was recorded; null until the signer acknowledges the disclosure step.","type":["string","null"]},"declined_at":{"type":["string","null"]},"document_content":{"description":"Markdown snapshot of the exact content being signed.","type":["string","null"]},"document_sha256":{"type":["string","null"]},"document_title":{"type":["string","null"]},"esign_disclosure":{"description":"Canonical electronic-records disclosure text (single source: sc-legal-templates). Server-provided so web + iOS render identical copy; present whenever state=ok.","type":["string","null"]},"esign_disclosure_version":{"description":"Version tag of esign_disclosure, recorded at consent time.","type":["string","null"]},"expires_at":{"type":["string","null"]},"signed_at":{"type":["string","null"]},"signer_email":{"type":["string","null"]},"signer_name":{"type":["string","null"]},"state":{"enum":["ok","not_found","voided","expired"],"type":"string"},"status":{"enum":["pending","viewed","signed","declined",null],"type":["string","null"]}},"required":["state"],"type":"object"},"SessionResponse":{"properties":{"authenticated":{"type":"boolean"},"user":{"$ref":"#/components/schemas/SessionUser"}},"required":["authenticated"],"type":"object"},"SessionUser":{"properties":{"activeTeamId":{"format":"uuid","type":"string"},"displayName":{"type":"string"},"email":{"format":"email","type":"string"},"id":{"format":"uuid","type":"string"},"role":{"enum":["user","admin","owner"],"type":"string"}},"required":["id","email","displayName"],"type":"object"},"SignatureRequestSummary":{"description":"Sender-side view of one e-signature request (#633). sign_path is the relative public signing path (/sign/<token>).","properties":{"created_at":{"type":"string"},"decline_reason":{"type":["string","null"]},"declined_at":{"type":["string","null"]},"document_title":{"type":"string"},"expires_at":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"proposal_id":{"format":"uuid","type":["string","null"]},"sign_path":{"type":"string"},"signed_at":{"type":["string","null"]},"signer_email":{"type":"string"},"signer_name":{"type":"string"},"status":{"enum":["pending","viewed","signed","declined","voided"],"type":"string"},"viewed_at":{"type":["string","null"]}},"required":["id","document_title","signer_name","signer_email","status","sign_path","created_at"],"type":"object"},"SignedDocumentDownload":{"description":"Executed-document PDF payload for a signed signature request. Wire shape of apps/web-leptos server_fns/crm/signatures.rs download_signed_document.","properties":{"file_name":{"description":"Suggested download filename (e.g. signed-<title>.pdf).","type":"string"},"pdf_base64":{"description":"Base64-encoded PDF bytes. Caller decodes + saves/shares.","type":"string"}},"required":["pdf_base64","file_name"],"type":"object"},"StormAlert":{"properties":{"createdAt":{"format":"date-time","type":"string"},"enabled":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"kinds":{"items":{"enum":["hail","wind","tornado","flood","severe_thunderstorm"],"type":"string"},"type":"array"},"label":{"type":"string"},"stateCode":{"maxLength":2,"minLength":2,"type":"string"},"zipCode":{"pattern":"^[0-9]{5}$","type":"string"}},"required":["id","label","enabled","createdAt"],"type":"object"},"StormAlertCreate":{"properties":{"kinds":{"default":["hail","wind","tornado"],"items":{"enum":["hail","wind","tornado","flood","severe_thunderstorm"],"type":"string"},"type":"array"},"label":{"type":"string"},"stateCode":{"maxLength":2,"minLength":2,"type":"string"},"zipCode":{"pattern":"^[0-9]{5}$","type":"string"}},"required":["label"],"type":"object"},"StormEvent":{"properties":{"id":{"format":"uuid","type":"string"},"kind":{"enum":["hail","wind","tornado","flood","severe_thunderstorm"],"type":"string"},"location":{"properties":{"countyName":{"type":"string"},"latitude":{"format":"double","type":"number"},"longitude":{"format":"double","type":"number"},"placeName":{"type":"string"},"stateCode":{"maxLength":2,"minLength":2,"type":"string"}},"required":["latitude","longitude"],"type":"object"},"occurredAt":{"format":"date-time","type":"string"},"severity":{"enum":["minor","moderate","severe","extreme"],"type":"string"},"summary":{"type":"string"}},"required":["id","kind","summary","occurredAt"],"type":"object"},"StormFeed":{"properties":{"cursor":{"description":"Opaque cursor; pass via `?cursor=` for the next page.","type":"string"},"events":{"items":{"$ref":"#/components/schemas/StormEvent"},"type":"array"},"generatedAt":{"format":"date-time","type":"string"}},"required":["events","generatedAt"],"type":"object"},"StormReportPurchase":{"properties":{"city":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"downloadCount":{"minimum":0,"type":"integer"},"endDate":{"description":"ISO-8601 YYYY-MM-DD","format":"date","type":"string"},"generationCompletedAt":{"format":"date-time","type":"string"},"generationStatus":{"enum":["pending","processing","completed","failed"],"type":"string"},"hailEventsCount":{"type":"integer"},"id":{"format":"uuid","type":"string"},"pageCount":{"type":"integer"},"paymentStatus":{"enum":["pending","paid","refunded","failed"],"type":"string"},"reportId":{"type":"string"},"searchRadiusMiles":{"maximum":50,"minimum":1,"type":"integer"},"startDate":{"description":"ISO-8601 YYYY-MM-DD","format":"date","type":"string"},"stateAbbreviation":{"maxLength":2,"minLength":2,"type":"string"},"tornadoEventsCount":{"type":"integer"},"totalEventsFound":{"type":"integer"},"windEventsCount":{"type":"integer"},"zipCode":{"pattern":"^[0-9]{5}$","type":"string"}},"required":["id","stateAbbreviation","zipCode","searchRadiusMiles","startDate","endDate","paymentStatus","generationStatus","downloadCount","createdAt"],"type":"object"},"StormReportReviewStatusOutput":{"description":"Wire shape of apps/web-leptos server_fns/storm_reports.rs StormReportReviewStatusOutput — the FINAL-GATE polling read.","properties":{"page_count":{"description":"Pages the reviewer inspected, once completed.","nullable":true,"type":"integer"},"pages_done":{"description":"Pages rendered so far (live render batches).","nullable":true,"type":"integer"},"pages_total":{"description":"Total pages being rendered/reviewed, once probed.","nullable":true,"type":"integer"},"review_seconds":{"description":"Completed-review duration in seconds — present only on green/released_manual (drives the \"reviewed in 1m 42s\" completion stat).","nullable":true,"type":"integer"},"stage":{"description":"Live worker milestone (#95) while status=pending. Null = queued (no telemetry yet) or a pre-telemetry review row. Additive — clients that ignore it keep working.","enum":["preparing","verifying_artifact","rendering_pages","reviewing","finalizing",null],"nullable":true,"type":"string"},"stage_detail":{"description":"Worker-authored work-naming line for the active stage (\"Rendered page 8 of 14\"). Customer-safe copy.","nullable":true,"type":"string"},"started_at":{"description":"Start of the latest review attempt.","format":"date-time","nullable":true,"type":"string"},"status":{"description":"Latest review state. A purchase with no review row yet reads as \"pending\" (no-review never releases).","enum":["pending","green","red","error","released_manual"],"type":"string"},"summary":{"description":"Reviewer's overall assessment — present ONLY on green; held verdicts expose no issue detail to the customer.","nullable":true,"type":"string"},"updated_at":{"description":"Latest review-row update; null when no review row exists yet.","format":"date-time","nullable":true,"type":"string"}},"required":["status"],"type":"object"},"UserProfile":{"properties":{"createdAt":{"format":"date-time","type":"string"},"displayName":{"type":"string"},"email":{"format":"email","type":"string"},"id":{"format":"uuid","type":"string"},"teams":{"items":{"properties":{"id":{"format":"uuid","type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"type":"array"}},"required":["id","email","displayName"],"type":"object"}},"securitySchemes":{"bearerAuth":{"bearerFormat":"ApiKey","description":"Session access token returned by `/api/auth/login` when\n`X-Client-Platform: ios` — the working Bearer credential today.\n(Account API keys mint with the `cbk_live_` prefix via\n/account/api-keys, but key-authenticated access to this surface\nis not yet wired; see #608/#611 audit notes.)\n","scheme":"bearer","type":"http"},"cookieAuth":{"in":"cookie","name":"user_session","type":"apiKey"}}},"info":{"contact":{"name":"SkyCanvass team","url":"https://skycanvass.com"},"description":"Public API surface for SkyCanvass — backend for the iOS companion app\nand external integrations. All endpoints are served by the Leptos shell\nat `apps/web-leptos/`. Auth is via session cookie (web) or `Authorization:\nBearer <access token>` (iOS post-login; see securitySchemes.bearerAuth).\n\nAccount endpoints backed by Leptos `#[server]` fns are POST +\nform-urlencoded (the macro's `PostUrl` codec) even when semantically\nread-only — verified against production 2026-06-10 (GET returns 405).\n\n**Status:** v0.1 hand-authored. Codegen-from-Zod pipeline forthcoming —\nsee `packages/api-spec/scripts/README.md` for the planned flow.\n","title":"SkyCanvass Public API","version":"0.1.0"},"openapi":"3.1.1","paths":{"/api/account/permits":{"post":{"operationId":"listAccountPermits","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PermitRow"},"type":"array"}}},"description":"Permit list"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"List the user's recent permit lookups","tags":["account","permits"]}},"/api/account/photo_packets":{"post":{"operationId":"listAccountPhotoPackets","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PhotoPacketRow"},"type":"array"}}},"description":"Photo packets"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"List the user's photo packets (canvassing route bundles)","tags":["account","photos"]}},"/api/account/profile":{"post":{"operationId":"getAccountProfile","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfile"}}},"description":"Profile snapshot"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Authenticated user profile","tags":["account"]}},"/api/account/storm_alerts":{"post":{"operationId":"listAccountStormAlerts","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/StormAlert"},"type":"array"}}},"description":"Storm-alert subscriptions"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"List the user's saved storm-alert subscriptions","tags":["account","storms"]}},"/api/account/storm_alerts/add":{"post":{"operationId":"addAccountStormAlert","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormAlertCreate"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormAlert"}}},"description":"Created alert"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Add a new storm-alert subscription","tags":["account","storms"]}},"/api/account/storm_alerts/remove":{"post":{"operationId":"removeAccountStormAlert","requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"format":"uuid","type":"string"}},"required":["id"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Removed"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Remove an alert subscription","tags":["account","storms"]}},"/api/account/storm_alerts/toggle":{"post":{"operationId":"toggleAccountStormAlert","requestBody":{"content":{"application/json":{"schema":{"properties":{"enabled":{"type":"boolean"},"id":{"format":"uuid","type":"string"}},"required":["id","enabled"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormAlert"}}},"description":"Updated alert"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Toggle an alert's enabled state","tags":["account","storms"]}},"/api/account/storm_feed":{"post":{"description":"iOS canvasser feed (#135). Returns the most recent N storm events\nfrom the last 30 days, optionally filtered to a single USPS state\ncode. Auth-gated. Response shape matches StormFeed for compatibility\nwith the swift-openapi-generator client.\n\n**Method is POST + form-urlencoded** because the Leptos `#[server]`\nmacro uses the `PostUrl` codec by default — server-fn arguments\nflow as form-encoded body, not query string. This is the\nconvention across every server-fn surface in this codebase;\ndo not \"fix\" to GET without first changing the server-fn macro\nto `input = GetUrl` (and verifying Leptos 0.8 supports it for\n`Option<T>` arg shapes — current macro doesn't).\n","operationId":"listAccountStormFeed","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"limit":{"default":25,"maximum":100,"minimum":1,"type":"integer"},"state":{"description":"USPS two-letter state code filter (e.g. FL, VA).","maxLength":2,"minLength":2,"pattern":"^[A-Z]{2}$","type":"string"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormFeed"}}},"description":"Recent storm events for the canvasser"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Feed of recent storm events for the canvasser surface","tags":["account","storms"]}},"/api/account/storm_reports":{"post":{"description":"Returns the user's PURCHASED storm reports (PDF download history).\nFor the canvasser feed of recent storm EVENTS near the user, see\n`/api/account/storm_feed` instead — the two surfaces are distinct.\nWeb `/account` UI consumes this endpoint; iOS uses storm_feed.\n","operationId":"listAccountStormReports","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/StormReportPurchase"},"type":"array"}}},"description":"Storm-report purchase history for the signed-in user"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"List the signed-in user's purchased storm reports","tags":["account","storms"]}},"/api/audit/auto_actions":{"post":{"description":"Immutable record of every automated action the system took or proposed for the caller (#52) — document auto-routes, payment proposals — with confirmation/reversal state and a server-side reversible verdict.","operationId":"listAutoActions","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"limit":{"maximum":200,"minimum":1,"type":"integer"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AutoAction"},"type":"array"}}},"description":"Audit rows"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List the automated-actions audit trail (newest first)","tags":["audit"]}},"/api/audit/reverse":{"post":{"description":"payment_proposed reverses by voiding the proposed payment; document_routed reverses by clearing the document's routing back to needs_review. The audit row gains reversed_at/reversed_by. Already-reversed rows and non-reversible action types fail with a validation error.","operationId":"reverseAutoAction","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"action_id":{"format":"uuid","type":"string"}},"required":["action_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoAction"}}},"description":"The reversed audit row"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Reverse an automated action where reversible","tags":["audit"]}},"/api/auth/login":{"post":{"description":"Returns `Set-Cookie: user_session=<token>` on success. iOS clients\n(header `X-Client-Platform: ios`) also receive `accessToken` in\nthe body for Bearer use across cookie-less native sessions.\n","operationId":"postAuthLogin","parameters":[{"description":"When `ios`, the response body includes `accessToken` for Bearer reuse.","in":"header","name":"X-Client-Platform","required":false,"schema":{"enum":["web","ios"],"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}},"description":"Logged in"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Wrong credentials"}},"security":[],"summary":"Email + password login","tags":["auth"]}},"/api/auth/logout":{"post":{"description":"Clears the `user_session` cookie + deletes the session row.\n303 to home. POST-only — the historical GET shim for\n`<a href=\"/api/auth/logout\">` anchor patterns was removed per\naudit #152 (pre-launch posture, zero consumers across the repo\nor iOS).\n","operationId":"postAuthLogout","responses":{"303":{"description":"Logged out — redirect to home"}},"security":[{},{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Sign out","tags":["auth"]}},"/api/auth/session":{"get":{"description":"Returns `{ authenticated: boolean, user?: SessionUser }`. Used by\niOS bootstrap to decide between sign-in vs main UI. Also used by\nthe marketing inline script for the CTA swap (#36).\n","operationId":"getAuthSession","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}},"description":"Session snapshot (authentication state in body, not status)"}},"security":[{},{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Current session info (always 200)","tags":["auth"]}},"/api/capture_links/complete":{"post":{"operationId":"tokenCompleteCapture","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[capture_run_id]":{"format":"uuid","type":"string"},"req[token]":{"type":"string"}},"required":["req[token]","req[capture_run_id]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRunSummary"}}},"description":"Completed run"}},"security":[],"summary":"Complete a capture_run via capture token (public, token-gated)","tags":["captures","qr","public"]}},"/api/capture_links/list_for_job":{"post":{"operationId":"listCaptureLinksForJob","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"job_id":{"format":"uuid","type":"string"}},"required":["job_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CaptureLinkSummary"},"type":"array"}}},"description":"Links"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Capture links for one job, newest first","tags":["captures","qr"]}},"/api/capture_links/mint":{"post":{"description":"Creates a job-scoped public capture token (encode https://skycanvass.com + capture_path as the QR payload). Bracket form: input[job_id], input[label], input[expires_days] (1-365, default 30).","operationId":"mintCaptureLink","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[expires_days]":{"maximum":365,"minimum":1,"type":"integer"},"input[job_id]":{"format":"uuid","type":"string"},"input[label]":{"maxLength":200,"type":"string"}},"required":["input[job_id]","input[label]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureLinkSummary"}}},"description":"Minted link"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Mint a QR capture link for a job (work-order photo uploads)","tags":["captures","qr"]}},"/api/capture_links/presign_upload":{"post":{"description":"Token mirror of /api/captures/presign_upload. Bracket form: req[token], req[capture_run_id], req[client_id], req[content_type].","operationId":"tokenPresignUpload","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[capture_run_id]":{"format":"uuid","type":"string"},"req[client_id]":{"type":"string"},"req[content_type]":{"enum":["image/jpeg","image/png","image/webp"],"type":"string"},"req[token]":{"type":"string"}},"required":["req[token]","req[capture_run_id]","req[client_id]","req[content_type]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CapturePresignedUpload"}}},"description":"Presigned URL pair (same shape as /api/captures/presign_upload)"}},"security":[],"summary":"Presign R2 PUT URLs via capture token (public, token-gated)","tags":["captures","qr","public"]}},"/api/capture_links/register":{"post":{"description":"Token mirror of /api/captures/register — single-level bracket form req[token] + every RegisterCapturePhoto field under req[...].","operationId":"tokenRegisterPhoto","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[camera_make]":{"type":"string"},"req[camera_model]":{"type":"string"},"req[caption]":{"type":"string"},"req[capture_run_id]":{"format":"uuid","type":"string"},"req[captured_at]":{"type":"string"},"req[client_id]":{"type":"string"},"req[file_size_bytes]":{"type":"integer"},"req[height]":{"type":"integer"},"req[latitude]":{"type":"number"},"req[longitude]":{"type":"number"},"req[mime_type]":{"type":"string"},"req[orientation]":{"type":"integer"},"req[original_sha256]":{"type":"string"},"req[r2_key]":{"type":"string"},"req[r2_thumbnail_key]":{"type":"string"},"req[template_step_id]":{"type":"string"},"req[token]":{"type":"string"},"req[width]":{"type":"integer"}},"required":["req[token]","req[capture_run_id]","req[client_id]","req[captured_at]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRegisteredPhoto"}}},"description":"Registered photo (same shape as /api/captures/register)"}},"security":[],"summary":"Register an uploaded photo via capture token (public, token-gated)","tags":["captures","qr","public"]}},"/api/capture_links/resolve":{"post":{"description":"Backs the post-QR-scan screen. Non-live links resolve as state=not_found (indistinguishable from never-existed). No ids are exposed — the token itself authorizes the capture operations below.","operationId":"resolveCaptureLink","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"token":{"maxLength":64,"minLength":64,"type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCaptureView"}}},"description":"Resolution"}},"security":[],"summary":"Resolve a capture token to its job labels (public, token-gated)","tags":["captures","qr","public"]}},"/api/capture_links/revoke":{"post":{"operationId":"revokeCaptureLink","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"link_id":{"format":"uuid","type":"string"}},"required":["link_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureLinkSummary"}}},"description":"Revoked link"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Revoke a live capture link","tags":["captures","qr"]}},"/api/capture_links/start_capture":{"post":{"operationId":"tokenStartCapture","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"token":{"type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRunSummary"}}},"description":"Opened run (same shape as /api/captures/start)"}},"security":[],"summary":"Open a capture_run via capture token (public, token-gated)","tags":["captures","qr","public"]}},"/api/crm/create_signature_request":{"post":{"description":"Snapshots the proposal content + SHA-256 at request time and mints a 64-hex sign token. Form-encoded with the serde_qs bracket convention (input[proposal_id]=...&input[signer_name]=...).","operationId":"createSignatureRequest","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[expires_days]":{"maximum":90,"minimum":1,"type":"integer"},"input[proposal_id]":{"format":"uuid","type":"string"},"input[signer_email]":{"format":"email","type":"string"},"input[signer_name]":{"type":"string"}},"required":["input[proposal_id]","input[signer_name]","input[signer_email]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignatureRequestSummary"}}},"description":"Created signature request"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Create an e-signature request for a proposal (#633)","tags":["crm","signatures"]}},"/api/crm/download_signed_document":{"post":{"description":"Sender-side download of the executed document (signed snapshot + signature image + audit certificate). Only valid for requests in status=signed; the caller must belong to the owning team.","operationId":"downloadSignedDocument","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"request_id":{"format":"uuid","type":"string"}},"required":["request_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedDocumentDownload"}}},"description":"Signed-document PDF payload"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Download the signed-document PDF for a completed signature request","tags":["crm","signatures"]}},"/api/crm/list_my_proposals_with_lead":{"post":{"operationId":"listCrmProposalsWithLead","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ProposalSummaryWithLead"},"type":"array"}}},"description":"Proposals with lead context"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Team's proposals JOIN-enriched with lead business name + location","tags":["crm","signatures"]}},"/api/crm/list_proposal_signature_requests":{"post":{"operationId":"listProposalSignatureRequests","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"proposal_id":{"format":"uuid","type":"string"}},"required":["proposal_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SignatureRequestSummary"},"type":"array"}}},"description":"Signature requests, newest first"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Signature requests attached to one proposal","tags":["crm","signatures"]}},"/api/crm/void_signature_request":{"post":{"operationId":"voidSignatureRequest","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"request_id":{"format":"uuid","type":"string"}},"required":["request_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignatureRequestSummary"}}},"description":"Voided request"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Cancel a live (pending/viewed) signature request","tags":["crm","signatures"]}},"/api/documents/get":{"post":{"operationId":"getDocument","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"document_id":{"format":"uuid","type":"string"}},"required":["document_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentDetail"}}},"description":"Document detail"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"One document + its per-engine extraction passes","tags":["documents"]}},"/api/documents/list":{"post":{"operationId":"listDocuments","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"limit":{"maximum":200,"minimum":1,"type":"integer"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"documents":{"items":{"$ref":"#/components/schemas/DocumentSummary"},"type":"array"}},"required":["documents"],"type":"object"}}},"description":"Documents"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List the signed-in user's documents (newest first)","tags":["documents"]}},"/api/documents/presign_upload":{"post":{"description":"Bracket form: req[client_id], req[content_type]. The key namespace is pinned to documents/{user_id}/{client_id}.{ext}; the original bytes are stored unmodified.","operationId":"presignDocumentUpload","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[client_id]":{"type":"string"},"req[content_type]":{"enum":["application/pdf","image/jpeg","image/png","image/webp"],"type":"string"}},"required":["req[client_id]","req[content_type]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentPresignResponse"}}},"description":"Presigned PUT URL + the canonical key to echo back"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Presign R2 PUT for a document upload (smart ingestion intake)","tags":["documents"]}},"/api/documents/register":{"post":{"description":"Bracket form under req[...]. The echoed r2_key must match the key presign minted for this user + client_id. Registers status \"uploaded\"; the ingestion worker takes it from there.","operationId":"registerDocument","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[client_id]":{"type":"string"},"req[file_size_bytes]":{"type":"integer"},"req[mime_type]":{"enum":["application/pdf","image/jpeg","image/png","image/webp"],"type":"string"},"req[original_filename]":{"type":"string"},"req[r2_key]":{"type":"string"},"req[sha256]":{"type":"string"},"req[uploaded_via]":{"enum":["ios","web"],"type":"string"}},"required":["req[client_id]","req[r2_key]","req[mime_type]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSummary"}}},"description":"The registered (or refreshed) document row"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Record an uploaded document (idempotent on client_id)","tags":["documents"]}},"/api/export/document_urls":{"post":{"description":"Documents sibling of /api/export/photo_urls — 1h presigned R2 GET URLs for ingested-paperwork originals, offset-paged. Bracket form req[limit], req[offset].","operationId":"exportDocumentUrls","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[limit]":{"maximum":500,"minimum":1,"type":"integer"},"req[offset]":{"minimum":0,"type":"integer"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"expires_in_seconds":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"returned":{"type":"integer"},"urls":{"items":{"properties":{"created_at":{"type":"string"},"customer_id":{"format":"uuid","type":["string","null"]},"document_id":{"format":"uuid","type":"string"},"download_url":{"type":"string"},"file_size_bytes":{"type":["integer","null"]},"job_id":{"format":"uuid","type":["string","null"]},"mime_type":{"type":"string"},"original_filename":{"type":["string","null"]},"r2_key":{"type":"string"}},"required":["document_id","r2_key","mime_type","created_at","download_url"],"type":"object"},"type":"array"}},"required":["urls","limit","offset","returned","expires_in_seconds"],"type":"object"}}},"description":"Presigned document download URLs"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Presigned GET batches for the caller's document originals","tags":["export"]}},"/api/export/my_data":{"post":{"description":"Data-freedom export (#29). Returns a complete machine-readable bundle of the caller's records: profile basics, jobs, capture runs, photo-asset records (metadata + R2 keys, not bytes), documents + extraction passes, purchased report records, photo packets, canvas and scanned documents, storm-alert subscriptions, saved areas + alerts, and — when the caller has an active team — the CRM book (leads, customers, proposals, activities, tasks, renewals, payments, signature requests). The manifest section carries per-entity counts, generated_at, and schema_version \"1\". Photo bytes are retrievable via /api/export/photo_urls.","operationId":"exportMyData","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportBundle"}}},"description":"Complete export bundle"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Export everything the signed-in user owns as one JSON bundle","tags":["export","account"]}},"/api/export/photo_urls":{"post":{"description":"Offset-paged batches of presigned R2 GET URLs so the photo BYTES are exportable, not just the records. Stable (captured_at, id) ordering; `returned < limit` signals the last page. Optional capture_run_id filter narrows to one capture run.","operationId":"exportPhotoUrls","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[capture_run_id]":{"format":"uuid","type":"string"},"req[limit]":{"default":100,"maximum":500,"minimum":1,"type":"integer"},"req[offset]":{"default":0,"minimum":0,"type":"integer"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportPhotoUrlsResponse"}}},"description":"One page of presigned photo download URLs"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Presigned GET URLs (1h) for the caller's photo originals","tags":["export","photos"]}},"/api/health":{"get":{"description":"Always-200 health endpoint. Body includes commit SHA, build profile\n(release|release-fast — release-fast retired #18), LTO mode, codegen\nunits, and DB ping latency. Used by deploy automation + uptime\nmonitoring + iOS bootstrap.\n","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Health snapshot"}},"security":[],"summary":"Service health probe with build provenance","tags":["health"]}},"/api/material_orders/download_pdf":{"post":{"description":"Only finalized orders have PDFs — drafts return 400. A finalized\norder whose PDF upload failed mid-finalize heals here: the sheet\nis regenerated, uploaded, and stamped before presigning.\n","operationId":"downloadMaterialOrderPdf","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"id":{"format":"uuid","type":"string"}},"required":["id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterialOrderPdfDownload"}}},"description":"Presigned download URL + suggested filename"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"The order is still a draft"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Presigned R2 GET (1 hour) for a finalized order's PDF sheet","tags":["material-orders"]}},"/api/material_orders/finalize":{"post":{"description":"Freezes the draft (status `finalized` + `finalized_at`), compiles\nthe branded order-sheet PDF via pdf-engine\n(`templates/material-order.typ`), uploads it to R2 under\n`material_orders/{user_id}/{id}.pdf`, and stamps `pdf_r2_key`.\nOnly valid from `draft`; re-finalizing returns 400. If the PDF\nstep fails after the status flip, the order stays finalized\nwithout a PDF and `/download_pdf` regenerates it on demand.\n","operationId":"finalizeMaterialOrder","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[id]":{"format":"uuid","type":"string"}},"required":["input[id]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"order":{"$ref":"#/components/schemas/MaterialOrder"},"page_count":{"nullable":true,"type":"integer"}},"required":["order"],"type":"object"}}},"description":"The finalized order + engine-reported page count"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Already finalized, or the parent job no longer exists"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Finalize a draft and synchronously generate its PDF order sheet","tags":["material-orders"]}},"/api/material_orders/get":{"post":{"operationId":"getMaterialOrder","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"id":{"format":"uuid","type":"string"}},"required":["id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterialOrder"}}},"description":"The order"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"One material order by id (owner-scoped)","tags":["material-orders"]}},"/api/material_orders/list":{"post":{"operationId":"listMaterialOrders","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"job_id":{"format":"uuid","type":"string"}},"required":["job_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"orders":{"items":{"$ref":"#/components/schemas/MaterialOrder"},"type":"array"}},"required":["orders"],"type":"object"}}},"description":"Orders for the job"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"All material orders for one job (newest first, owner-scoped)","tags":["material-orders","jobs"]}},"/api/material_orders/preview":{"post":{"description":"Generates a supplier-ready material line-item list (shingle bundles,\nunderlayment, ice & water shield, drip edge, ridge cap, starter strip)\nfrom the LiDAR roof model nearest the job's geocoded coordinates. The\ncontractor reviews/tweaks the quantities instead of hand-building\nsupplier orders; each item carries a `basis` string recording the\nformula that produced its starting quantity.\n\nA job whose address has no geocoded coordinates, or whose coordinates\nfall outside LiDAR roof coverage, returns the typed `no_roof` state\nin a 200 response — NOT an error status.\n\n**Method is POST + form-urlencoded** (Leptos `#[server]` `PostUrl`\ncodec). The single-struct argument encodes as one-level bracketed\nkeys: `input[job_id]`, `input[waste_factor_pct]`,\n`input[half_edge_deg]`.\n","operationId":"previewMaterialOrder","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[half_edge_deg]":{"default":0,"description":"Nearest-roof bbox half-edge in decimal degrees. 0 defers\nto the parcel-scale default (0.0005 ≈ 55 m).\n","maximum":0.01,"minimum":0,"type":"number"},"input[job_id]":{"description":"The job (project) UUID. Ownership enforced server-side.","format":"uuid","type":"string"},"input[waste_factor_pct]":{"default":10,"description":"Shingle waste factor percent.","maximum":30,"minimum":0,"type":"integer"}},"required":["input[job_id]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"discriminator":{"mapping":{"no_roof":"#/components/schemas/MaterialOrderPreviewNoRoof","ok":"#/components/schemas/MaterialOrderPreviewOk"},"propertyName":"status"},"oneOf":[{"$ref":"#/components/schemas/MaterialOrderPreviewOk"},{"$ref":"#/components/schemas/MaterialOrderPreviewNoRoof"}]}}},"description":"Derived preview, or the typed no-roof coverage state"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Malformed job_id / out-of-range waste factor or bbox"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Job not found for this user (missing, deleted, or not owned)"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Derive an editable material order from a job's roof measurements","tags":["material-orders","jobs"]}},"/api/material_orders/save":{"post":{"description":"Creates a `draft` material_orders row from a preview the contractor\nedited. The server RE-DERIVES the roof measurements and line items\nfrom the job's nearest LiDAR roof; the client contributes only\nper-SKU quantity overrides and row removals via `items_json`.\nTotals are recomputed server-side through the calculators kernel —\nclient-computed totals are never accepted on the wire.\n\n`items_json` is a JSON-encoded array of\n`{\"sku_hint\": \"...\", \"quantity\": <number>}` objects. SKUs omitted\nfrom the array are removed from the order; unknown SKUs are\nrejected. Same `no_roof` coverage semantics as preview (200, typed).\n","operationId":"saveMaterialOrder","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[half_edge_deg]":{"default":0,"maximum":0.01,"minimum":0,"type":"number"},"input[items_json]":{"description":"JSON array of {sku_hint, quantity} edit objects (max 64 KB).","type":"string"},"input[job_id]":{"format":"uuid","type":"string"},"input[label]":{"maxLength":200,"minLength":1,"type":"string"},"input[waste_factor_pct]":{"default":10,"maximum":30,"minimum":0,"type":"integer"}},"required":["input[job_id]","input[label]","input[items_json]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"discriminator":{"mapping":{"no_roof":"#/components/schemas/MaterialOrderPreviewNoRoof","saved":"#/components/schemas/MaterialOrderSaved"},"propertyName":"status"},"oneOf":[{"$ref":"#/components/schemas/MaterialOrderSaved"},{"$ref":"#/components/schemas/MaterialOrderPreviewNoRoof"}]}}},"description":"The saved draft, or the typed no-roof coverage state"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Malformed input (bad UUID, label bounds, items_json shape)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Persist a contractor-edited preview as a draft material order","tags":["material-orders","jobs"]}},"/api/material_orders/update":{"post":{"description":"Applies the same `items_json` edit shape against the STORED draft\nitems, then recomputes totals through the calculators kernel.\nOnly `draft` orders are editable — a finalized order returns 400.\n","operationId":"updateMaterialOrder","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[id]":{"format":"uuid","type":"string"},"input[items_json]":{"description":"JSON array of {sku_hint, quantity} edit objects (max 64 KB).","type":"string"}},"required":["input[id]","input[items_json]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterialOrder"}}},"description":"The updated draft"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"description":"Malformed input, or the order is already finalized"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Tweak a draft order's quantities (server-side re-total)","tags":["material-orders"]}},"/api/payments/confirm":{"post":{"description":"THE confirmation gate (decision #14) — the only path from a system-proposed payment to confirmed money. Stamps confirmed_at/confirmed_by and marks the matching payment_proposed audit row confirmed. Fails with a validation error when the payment was already confirmed or voided.","operationId":"confirmPayment","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"payment_id":{"format":"uuid","type":"string"}},"required":["payment_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}},"description":"The confirmed payment"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Confirm a proposed payment (proposed -> confirmed)","tags":["payments"]}},"/api/payments/create":{"post":{"description":"Bracket form under req[...]. A human typing a payment IS the confirmation, so the row is created status=confirmed with the stamps set. amount_cents must be positive; the job must belong to the caller.","operationId":"createPayment","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"req[amount_cents]":{"minimum":1,"type":"integer"},"req[customer_id]":{"format":"uuid","type":"string"},"req[job_id]":{"format":"uuid","type":"string"},"req[method]":{"enum":["check","ach","wire","card","cash","other"],"type":"string"},"req[notes]":{"type":"string"},"req[received_at]":{"format":"date","type":"string"},"req[reference]":{"maxLength":100,"type":"string"}},"required":["req[job_id]","req[amount_cents]","req[method]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}},"description":"The recorded (confirmed) payment"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Record a manual payment (born confirmed)","tags":["payments"]}},"/api/payments/for_customer":{"post":{"operationId":"listCustomerPayments","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"customer_id":{"format":"uuid","type":"string"}},"required":["customer_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobPaymentsResponse"}}},"description":"Payments (all statuses) + confirmed-only total"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Payments ledger for one customer + confirmed total","tags":["payments"]}},"/api/payments/for_job":{"post":{"operationId":"listJobPayments","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"job_id":{"format":"uuid","type":"string"}},"required":["job_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobPaymentsResponse"}}},"description":"Payments (all statuses) + confirmed-only total"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Payments ledger for one job + confirmed total","tags":["payments"]}},"/api/payments/proposed":{"post":{"description":"The review inbox (#52, decision #14): payments the ingestion pipeline PROPOSED from scanned checks routed onto a job. Oldest first, with the job street address for the confirmation prompt. Nothing here is money yet — only /api/payments/confirm makes it so.","operationId":"listProposedPayments","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"limit":{"maximum":100,"minimum":1,"type":"integer"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Payment"},"type":"array"}}},"description":"Proposed payments"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List system-proposed payments awaiting human confirmation","tags":["payments"]}},"/api/payments/void":{"post":{"description":"Voids a still-proposed payment and marks the matching audit row reversed. Confirmed payments cannot be voided through this path.","operationId":"voidPayment","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"payment_id":{"format":"uuid","type":"string"}},"required":["payment_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}},"description":"The voided payment"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Dismiss a proposed payment (proposed -> voided)","tags":["payments"]}},"/api/sign/consent":{"post":{"description":"The affirmative consent action taken after the signer reads the electronic-records disclosure (PublicSignView.esign_disclosure) and BEFORE the signature pad unlocks. Recorded separately from the signature submission — stamps signature_requests.consented_at and appends a distinct `consented` signature_events row carrying the disclosure version. Idempotent: repeat calls return state=consented without duplicating the audit event.","operationId":"recordSignConsent","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"token":{"maxLength":64,"minLength":64,"type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSignResult"}}},"description":"Consent outcome (state=consented on success)"}},"security":[],"summary":"Record ESIGN §101(c) consent to proceed electronically (public, token-gated)","tags":["signatures","public"]}},"/api/sign/decline":{"post":{"operationId":"declineSignRequest","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"reason":{"maxLength":1000,"type":"string"},"token":{"type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSignResult"}}},"description":"Decline outcome"}},"security":[],"summary":"Decline a signature request (public, token-gated)","tags":["signatures","public"]}},"/api/sign/lookup":{"post":{"description":"The sign token IS the credential — no session required. First lookup of a pending request promotes it to viewed (+ audit event).","operationId":"lookupSignRequest","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"token":{"maxLength":64,"minLength":64,"type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSignView"}}},"description":"Document snapshot or terminal state"}},"security":[],"summary":"Resolve a sign token to its document snapshot (public, token-gated)","tags":["signatures","public"]}},"/api/sign/submit":{"post":{"description":"signature_png must be a data:image/png;base64 URL between 1000 chars and 300 KB; consent must be true. Signing a proposal-backed request transactionally promotes the proposal to accepted and the lead to won.","operationId":"submitSignature","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"consent":{"type":"boolean"},"signature_png":{"type":"string"},"token":{"type":"string"}},"required":["token","signature_png","consent"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSignResult"}}},"description":"Submission outcome"}},"security":[],"summary":"Submit a drawn signature (public, token-gated)","tags":["signatures","public"]}},"/api/storm_reports/generate":{"post":{"description":"Renders the storm-report Typst template through pdf-engine (#27/#186/#243). Commercial path (#49): auth-gated, costs 1 report credit (atomic check + deduct; a short balance returns 402), persists a storm_report_purchases row (the report appears in /account/storm-reports), and uploads the PDF artifact to R2. FINAL GATE (2026-06-12 beta directive): the response NO LONGER carries pdf_base64 — the artifact enters an autonomous visual quality review (pdf_reviews ledger) and the response returns review_status \"pending\" + the gated download_url. Poll /api/storm_reports/review_status until green, then GET download_url. input[state] + input[zip] are required. Exactly one of input[contour_id] / input[focus_date] must be provided — supplying both or neither is a validation error. Form-encoded with the serde_qs bracket convention because the server fn takes a single struct argument (input[property_lat]=...&input[focus_date]=...).","operationId":"generateStormReport","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"input[address]":{"description":"Optional display address line for the PDF cover and the purchase row's street_address.","type":"string"},"input[contour_id]":{"description":"Optional mesh_contour UUID — surfaced as sourceContourId in the rendered envelope. Mutually exclusive with input[focus_date].","format":"uuid","type":"string"},"input[end_date]":{"description":"Inclusive storm-window upper bound (YYYY-MM-DD).","format":"date","type":"string"},"input[focus_date]":{"description":"Storm date (YYYY-MM-DD). Mutually exclusive with input[contour_id].","format":"date","type":"string"},"input[include_radar]":{"default":true,"description":"Whether the report includes NEXRAD/MRMS radar panels. Defaults true when omitted.","type":"boolean"},"input[property_lat]":{"description":"Property latitude — anchors map viewport + LSR window.","format":"double","type":"number"},"input[property_lon]":{"description":"Property longitude.","format":"double","type":"number"},"input[radius_mi]":{"default":0,"description":"Search radius in miles for the property-vicinity viewport. Omitted/zero falls back to 15 mi server-side.","format":"double","type":"number"},"input[start_date]":{"description":"Inclusive storm-window lower bound (YYYY-MM-DD). With input[end_date], overrides the single-day focus_date collapse in the compiled envelope.","format":"date","type":"string"},"input[state]":{"description":"2-letter state abbreviation for the persisted purchase row. Required (#49 commercial path).","maxLength":2,"minLength":2,"type":"string"},"input[zip]":{"description":"5-digit ZIP for the persisted purchase row. Required (#49 commercial path).","pattern":"^[0-9]{5}$","type":"string"}},"required":["input[property_lat]","input[property_lon]","input[state]","input[zip]"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateStormReportOutput"}}},"description":"Order accepted — purchase persisted, artifact uploaded, review opened. pdf_base64 is absent; review_status is \"pending\"."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Insufficient report credits — the typed AppError::InsufficientCredits body carries the required cost and the caller's current balance. Buy credits at /account/credits."}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Generate a storm-report PDF via the pdf-engine compile pipeline","tags":["storms","reports"]}},"/api/storm_reports/review_status":{"post":{"description":"Polling read for the autonomous quality review (2026-06-12 beta directive). Auth-gated + ownership-checked. Returns the latest pdf_reviews state for the purchase — pending | green | red | error | released_manual. A purchase with no review row yet reads as \"pending\" (no-review never releases; the download path opens one on first request). summary carries the reviewer's overall assessment ONLY on green — held verdicts expose no issue detail. Form-encoded (purchase_id=...).","operationId":"getStormReportReviewStatus","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"properties":{"purchase_id":{"format":"uuid","type":"string"}},"required":["purchase_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormReportReviewStatusOutput"}}},"description":"Latest review state for the purchase"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"FINAL-GATE review status for a purchased storm report","tags":["storms","reports"]}},"/api/storm_reports/{purchase_id}/download":{"get":{"description":"Serves the LOCKED R2 artifact (evidence integrity, #43) — the exact bytes whose SHA-256 was recorded at generation, echoed as X-Report-SHA256 — but ONLY after the FINAL GATE passes (2026-06-12 beta directive): the latest pdf_reviews row for this purchase must be green or released_manual. Requires ownership + payment_status='succeeded'. A purchase with no review on record gets one opened automatically (202); bytes whose hash drifted from the reviewed ones are re-reviewed instead of served.","operationId":"downloadStormReport","parameters":[{"in":"path","name":"purchase_id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/pdf":{"schema":{"format":"binary","type":"string"}}},"description":"The reviewed PDF (attachment; X-Report-SHA256 header)"},"202":{"content":{"application/json":{"schema":{"properties":{"status":{"const":"pending","type":"string"}},"required":["status"],"type":"object"}}},"description":"Review in flight — body {\"status\":\"pending\"}. Poll /api/storm_reports/review_status and retry the download."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Purchase not paid (payment_status != succeeded)."},"404":{"description":"No such purchase for this caller."},"409":{"content":{"application/json":{"schema":{"properties":{"message":{"type":"string"},"status":{"const":"held","type":"string"}},"required":["status","message"],"type":"object"}}},"description":"Held by the quality gate (review red or error) — body {\"status\":\"held\",\"message\":...}. The message is neutral customer copy; no issue detail crosses this boundary."}},"security":[{"cookieAuth":[]},{"bearerAuth":[]}],"summary":"Download a purchased storm report (gated by the FINAL GATE)","tags":["storms","reports"]}}},"servers":[{"description":"Production (canonical — iOS app pins here)","url":"https://skycanvass.com"}]}