{"openapi":"3.1.0","info":{"title":"Massalaskuri Partner API","description":"Upload a construction blueprint PDF → Massalaskuri AI detects electrical & fire-alarm symbols →\nyou get back every symbol with **normalized coordinates** plus a **quantity take-off** (counts by symbol & Talo 2010 code).\n\n👉 **See it live:** [interactive blueprint demo](/playground)  ·  **5-min integration kit:** [/examples](/examples) (sample PDF + bash/Node/Python + webhook receiver)  ·  **Status:** [/status](/status)\n\n## Quickstart\n1. `POST /v1/jobs/init` with `{ file_name, mime_type, size }` → returns `{ job_id, upload.url }`.\n2. `PUT` the file bytes to `upload.url` (Content-Type = your mime_type; no API key on this PUT).\n3. `POST /v1/jobs/{id}/start` → returns `202 queued`.\n4. Poll `GET /v1/jobs/{id}` until `status:\"done\"` — or set `callback_url` to receive a webhook.\n5. Read detections: `GET /v1/jobs/{id}/pages/{n}/detections`.\n\n## Authentication\nSend your key in the `X-API-Key` header on every request. Use the **Authentication** button\nabove to set it, then try any endpoint live.\n\n## Coordinates\nUse `bbox_norm` (0–1 fractions, top-left origin) to place boxes on any rendering of the page —\nit is DPI-independent. `bbox_px` (200-DPI raster) is provided for reference.\n\n## Webhooks\nIf you pass `callback_url`, we POST the completed job (signed `X-Massalaskuri-Signature: t=<ts>,v1=<hmac>`,\nwhere `v1 = HMAC_SHA256(your_webhook_secret, \"<t>.\" + rawBody)`). Verify it before trusting the payload.","version":"1.0.0","contact":{"name":"Massalaskuri Partner Support","email":"info@massalaskuri.fi","url":"https://api.massalaskuri.com"}},"servers":[{"url":"https://api.massalaskuri.com","description":"Production"}],"tags":[{"name":"Jobs","description":"Submit blueprints and retrieve detections + take-off."},{"name":"Symbols","description":"Symbol catalog and icons."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your partner API key."}},"schemas":{"InitResponse":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","example":"awaiting_upload"},"external_ref":{"type":["string","null"]},"upload":{"type":"object","properties":{"method":{"type":"string","const":"PUT"},"url":{"type":"string"},"content_type":{"type":"string"},"max_bytes":{"type":"number","example":52428800},"expires_at":{"type":"string"}},"required":["method","url","content_type","max_bytes","expires_at"]}},"required":["job_id","status","external_ref","upload"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"not_found"},"message":{"type":"string","example":"Job not found"},"details":{}},"required":["code","message"]}},"required":["error"]},"StartResponse":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","example":"queued"},"poll_url":{"type":"string"}},"required":["job_id","status","poll_url"]},"JobDone":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","const":"done"},"external_ref":{"type":["string","null"]},"completed_at":{"type":["string","null"]},"file_name":{"type":["string","null"]},"page_count":{"type":"number","example":1},"pages_with_detections":{"type":"number","example":1},"confidence_mode":{"type":"string","example":"per_class_default"},"inference_thresholds":{"type":"object","additionalProperties":{"type":"object","properties":{"confidence":{"type":"number"},"single_class":{"type":"number"}},"required":["confidence","single_class"]}},"summary":{"$ref":"#/components/schemas/TakeoffSummary"},"pages":{"type":"array","items":{"type":"object","properties":{"page_index":{"type":"number","example":0},"width_px":{"type":"number","example":1654},"height_px":{"type":"number","example":2339},"render_dpi":{"type":"number","example":200},"detection_count":{"type":"number","example":14},"detections_url":{"type":"string"},"image_url":{"type":["string","null"]},"detections":{"type":"array","items":{"$ref":"#/components/schemas/Detection"}}},"required":["page_index","width_px","height_px","render_dpi","detection_count","detections_url","image_url"]}}},"required":["job_id","status","external_ref","completed_at","file_name","page_count","pages_with_detections","confidence_mode","inference_thresholds","summary","pages"]},"TakeoffSummary":{"type":"object","properties":{"total_detections":{"type":"number","example":51},"by_symbol":{"type":"array","items":{"type":"object","properties":{"class":{"type":"string"},"display_name":{"type":["string","null"]},"talo2010_code":{"type":["string","null"]},"category":{"type":["string","null"]},"count":{"type":"number","example":13}},"required":["class","display_name","talo2010_code","category","count"]}},"by_category":{"type":"object","additionalProperties":{"type":"number"},"example":{"Valot":18,"Sireenit":9}}},"required":["total_detections","by_symbol","by_category"]},"Detection":{"type":"object","properties":{"id":{"type":"string","example":"a1b2c3d4-1111-2222-3333-444455556666"},"class":{"type":"string","example":"2os_pistorasia_uppo"},"display_name":{"type":["string","null"],"example":"2-os pistorasia uppo"},"category":{"type":["string","null"],"example":"Pistorasiat"},"symbol_category":{"type":["string","null"],"example":"electricity_symbols"},"color":{"type":["string","null"],"example":"#ee00ff"},"talo2010_code":{"type":["string","null"],"example":"S241"},"icon_url":{"type":["string","null"]},"confidence":{"type":"number","example":0.94},"bbox_norm":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"}},"required":["x","y","width","height"],"description":"Bounding box as fractions 0–1 of the page (top-left origin). Use this to place boxes regardless of render DPI."},"bbox_px":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"}},"required":["x","y","width","height"],"description":"200-DPI raster pixels (top-left origin)."},"source":{"type":"object","properties":{"yolo":{"type":"boolean"},"vector_matching":{"type":"boolean"}},"required":["yolo","vector_matching"]},"gemini":{"type":"object","properties":{"decision":{"type":["string","null"]},"confidence":{"type":["number","null"]}},"required":["decision","confidence"]}},"required":["id","class","display_name","category","symbol_category","color","talo2010_code","icon_url","confidence","bbox_norm","bbox_px","source"]},"PageDetections":{"type":"object","properties":{"page_index":{"type":"number"},"width_px":{"type":"number"},"height_px":{"type":"number"},"render_dpi":{"type":"number"},"confidence_mode":{"type":"string"},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"},"next":{"type":["number","null"]},"detections":{"type":"array","items":{"$ref":"#/components/schemas/Detection"}}},"required":["page_index","width_px","height_px","render_dpi","confidence_mode","total","limit","offset","next","detections"]},"SymbolCatalog":{"type":"object","properties":{"models":{"type":"array","items":{"type":"string"},"example":["fire_alarm","electricity"]},"count":{"type":"number","example":101},"symbols":{"type":"array","items":{"type":"object","properties":{"class":{"type":"string"},"display_name":{"type":["string","null"]},"category":{"type":["string","null"]},"symbol_category":{"type":["string","null"]},"color":{"type":["string","null"]},"talo2010_code":{"type":["string","null"]},"icon_url":{"type":["string","null"]}},"required":["class","display_name","category","symbol_category","color","talo2010_code","icon_url"]}}},"required":["models","count","symbols"]}}},"security":[{"ApiKeyAuth":[]}],"paths":{"/v1/jobs/init":{"post":{"operationId":"postV1JobsInit","tags":["Jobs"],"summary":"Create a job and get an upload URL","description":"Validates the file metadata and returns a short-lived signed URL to PUT the blueprint bytes to. Optional `Idempotency-Key` header.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"vendor":"zod"},"example":{"file_name":"plan.pdf","mime_type":"application/pdf","size":1090214,"external_ref":"offer-123"}}}},"responses":{"201":{"description":"Job created; upload the file next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitResponse"},"example":{"job_id":"2b980b35-2fda-4e3e-b73f-91bfe9b4fd37","status":"awaiting_upload","external_ref":"offer-123","upload":{"method":"PUT","url":"https://….supabase.co/storage/v1/object/upload/sign/pap-files/…?token=…","content_type":"application/pdf","max_bytes":52428800,"expires_at":"2026-06-04T15:05:19.430Z"}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key"}}}}},"422":{"description":"Unsupported mime_type / size / callback_url","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unprocessable","message":"Unsupported mime_type. Allowed: application/pdf, image/jpeg, image/jpg, image/png, image/webp"}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Limit 120/min exceeded"}}}}}}}},"/v1/jobs/{id}/start":{"post":{"operationId":"postV1JobsByIdStart","tags":["Jobs"],"summary":"Start processing an uploaded job","description":"Verifies the uploaded file and queues it for AI inference. Returns immediately; status moves queued → processing → done.","security":[{"ApiKeyAuth":[]}],"responses":{"202":{"description":"Queued for processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartResponse"},"example":{"job_id":"2b980b35-2fda-4e3e-b73f-91bfe9b4fd37","status":"queued","poll_url":"/v1/jobs/2b980b35-2fda-4e3e-b73f-91bfe9b4fd37"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key"}}}}},"409":{"description":"Already started/processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"conflict","message":"Job already processing"}}}}},"422":{"description":"No/invalid upload found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unprocessable","message":"No uploaded file found for this job. PUT the file to the upload URL first."}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Limit 120/min exceeded"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}]}},"/v1/jobs/{id}":{"get":{"operationId":"getV1JobsById","tags":["Jobs"],"summary":"Poll job status / get results","description":"While running, returns status only. When `done`, returns the take-off summary + per-page metadata. Query: `?confidence=` (flat override), `?include=detections` (inline detections for small jobs).","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Job status. Running states (`queued`/`processing`) return status only; `done` returns the take-off summary + pages; `failed` returns an error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobDone"},"examples":{"queued":{"summary":"Queued (just started, not processing yet)","value":{"job_id":"2b980b35-2fda-4e3e-b73f-91bfe9b4fd37","status":"queued","external_ref":"offer-123","created_at":"2026-06-04T15:05:10.000Z","started_at":null,"file_name":"plan.pdf","pages_total":null,"pages_done":null}},"processing":{"summary":"Processing (AI running)","value":{"job_id":"2b980b35-2fda-4e3e-b73f-91bfe9b4fd37","status":"processing","external_ref":"offer-123","created_at":"2026-06-04T15:05:10.000Z","started_at":"2026-06-04T15:05:40.000Z","file_name":"plan.pdf","pages_total":null,"pages_done":null}},"done":{"summary":"Done (results ready)","value":{"job_id":"2b980b35-2fda-4e3e-b73f-91bfe9b4fd37","status":"done","external_ref":"offer-123","completed_at":"2026-06-04T15:06:40.000Z","file_name":"plan.pdf","page_count":1,"pages_with_detections":1,"confidence_mode":"per_class_default","inference_thresholds":{"fire_alarm":{"confidence":0.1,"single_class":0.25},"electricity":{"confidence":0.1,"single_class":0.25}},"summary":{"total_detections":51,"by_symbol":[{"class":"opasvalo_25m_alas_sivulle","display_name":"Opasvalo 25m alas sivulle","talo2010_code":"S610","category":"Valot","count":13}],"by_category":{"Valot":18,"Sireenit":9,"Painikkeet":7}},"pages":[{"page_index":0,"width_px":11575,"height_px":4678,"render_dpi":200,"detection_count":51,"image_url":null,"detections_url":"https://api.massalaskuri.com/v1/jobs/2b980b35-2fda-4e3e-b73f-91bfe9b4fd37/pages/0/detections"}]}},"failed":{"summary":"Failed","value":{"job_id":"2b980b35-2fda-4e3e-b73f-91bfe9b4fd37","status":"failed","external_ref":"offer-123","completed_at":"2026-06-04T15:06:00.000Z","error":{"code":"processing_failed","message":"Failed to render PDF"}}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key"}}}}},"404":{"description":"Job not found (or not yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Job not found"}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Limit 120/min exceeded"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}]}},"/v1/jobs/{id}/pages/{page}/detections":{"get":{"operationId":"getV1JobsByIdPagesByPageDetections","tags":["Jobs"],"summary":"Per-page detections (paginated)","description":"Detections for one page (0-based). Query: `?limit=&offset=&confidence=`. Same confidence filter as the summary, so counts reconcile.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Paginated detections for the page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageDetections"},"example":{"page_index":0,"width_px":11575,"height_px":4678,"render_dpi":200,"confidence_mode":"per_class_default","total":51,"limit":500,"offset":0,"next":null,"detections":[{"id":"a1b2c3d4-1111-2222-3333-444455556666","class":"opasvalo_25m_alas_sivulle","display_name":"Opasvalo 25m alas sivulle","category":"Valot","symbol_category":"fire_alarm_symbols","color":"#ee2b79","talo2010_code":"S610","icon_url":"https://api.massalaskuri.com/v1/symbols/opasvalo_25m_alas_sivulle/icon","confidence":0.95,"bbox_norm":{"x":0.3727,"y":0.82343,"width":0.00553,"height":0.01026},"bbox_px":{"x":4314,"y":3852,"width":64,"height":48},"source":{"yolo":true,"vector_matching":false}}]}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key"}}}}},"404":{"description":"Job/page not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Page 0 not found"}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Limit 120/min exceeded"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"page","required":true}]}},"/v1/symbols":{"get":{"operationId":"getV1Symbols","tags":["Symbols"],"summary":"Symbol catalog","description":"The full dictionary of detectable symbols (class → display_name → Talo 2010 code → icon). Fetch once and cache; map each `class`/`talo2010_code` to a product in your registry.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Catalog of current symbols.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SymbolCatalog"},"example":{"models":["fire_alarm","electricity"],"count":101,"symbols":[{"class":"2os_pistorasia_uppo","display_name":"2-os pistorasia uppo","category":"Pistorasiat","symbol_category":"electricity_symbols","color":"#ee00ff","talo2010_code":"S241","icon_url":"https://api.massalaskuri.com/v1/symbols/2os_pistorasia_uppo/icon"}]}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key"}}}}}}}},"/v1/symbols/{class}/icon":{"get":{"operationId":"getV1SymbolsByClassIcon","tags":["Symbols"],"summary":"Symbol icon (image)","description":"Redirects (302) to an image of the symbol glyph. Safe to use as an <img> src.","security":[{"ApiKeyAuth":[]}],"responses":{"302":{"description":"Redirect to a signed icon image URL."},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key"}}}}},"404":{"description":"No icon for this symbol."}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"class","required":true}]}}}}