# Massalaskuri Partner API — Full reference (single-file, for AI assistants) > Upload a construction blueprint PDF → Massalaskuri AI detects electrical & fire-alarm symbols → > get back every symbol with coordinates + a quantity take-off. REST/JSON, async job model, > `X-API-Key` auth. Base URL: https://api.massalaskuri.com This file consolidates the flow, every endpoint, schemas, coordinates, confidence, errors, and webhook verification so an assistant can answer integration questions without fetching multiple pages. The machine-readable contract is at https://api.massalaskuri.com/openapi.json. ================================================================================ ## AUTH Every `/v1` request requires header `X-API-Key: `. Missing/invalid → 401. The signed-upload `PUT` (step 2) does NOT take the API key (the token is in the URL). ================================================================================ ## FLOW (async: init → upload → start → poll/webhook) 1. POST /v1/jobs/init body {file_name, mime_type, size, models?, confidence?, external_ref?, callback_url?} → 201 {job_id, status:"awaiting_upload", external_ref, upload:{method:"PUT", url, content_type, max_bytes, expires_at}} 2. PUT (raw bytes; Content-Type = your mime_type; NO api key) → 200 3. POST /v1/jobs/{job_id}/start → 202 {job_id, status:"queued", poll_url} 4. GET /v1/jobs/{job_id} → poll until status:"done" (or set callback_url for a webhook) 5. GET /v1/jobs/{job_id}/pages/{page_index}/detections?limit=&offset=&confidence= → paginated detections Job statuses: awaiting_upload, queued, processing, done, failed, expired. `start` returns immediately (status "queued"); processing begins within ~30s (server-side scheduler). ================================================================================ ## ENDPOINTS ### POST /v1/jobs/init Request JSON: - file_name (string, required) - mime_type (string, required; one of: application/pdf, image/png, image/jpeg, image/jpg, image/webp) - size (int bytes, required; max 52428800 = 50 MB) - models (string[], optional; subset of ["fire_alarm","electricity"]; default both) - confidence (number 0–1, optional; job-default flat display threshold; omit → per-class app defaults) - external_ref (string, optional; echoed on every response) - callback_url (string https, optional; receive a signed webhook on completion) Optional header: Idempotency-Key (repeat → same job). 201 → {job_id, status, external_ref, upload:{method,url,content_type,max_bytes,expires_at}} Errors: 401, 422 (bad mime/size/callback), 429. ### POST /v1/jobs/{job_id}/start 202 → {job_id, status:"queued", poll_url}. Errors: 401, 404, 409 (already started), 422 (no/invalid upload), 429. ### GET /v1/jobs/{job_id} Query: confidence (number 0–1, flat override), include=detections (inline detections; small jobs). Running → {job_id, status, external_ref, created_at, started_at, file_name, pages_total:null, pages_done:null} Done → {job_id, status:"done", external_ref, completed_at, file_name, page_count, pages_with_detections, confidence_mode, inference_thresholds, summary, pages[]} Failed → {job_id, status:"failed", error:{code,message}} Errors: 401, 404, 429. ### GET /v1/jobs/{job_id}/pages/{page_index}/detections (page_index 0-based) Query: limit (default 500, max 2000), offset, confidence. → {page_index, width_px, height_px, render_dpi, confidence_mode, total, limit, offset, next, detections[]} `next` = next offset or null. Errors: 401, 404, 429. ### GET /v1/symbols (cacheable catalog) → {models, count, symbols:[{class, display_name, category, symbol_category, color, talo2010_code, icon_url}]} Map `class` (stable machine key) or `talo2010_code` to your product registry. Errors: 401. ### GET /v1/symbols/{class}/icon 302 redirect to a symbol glyph image. Errors: 401, 404. ================================================================================ ## DETECTION OBJECT { id (uuid), class (string, Finnish machine key), display_name (string|null), category (string|null), symbol_category (string|null e.g. "electricity_symbols"|"fire_alarm_symbols"), color (hex|null), talo2010_code (string|null, Talo 2010 classification, ~75% coverage), icon_url (string|null), confidence (number 0–1), bbox_norm {x,y,width,height} // fractions 0–1 of page, TOP-LEFT origin — USE THIS bbox_px {x,y,width,height} // 200-DPI raster pixels (reference) source {yolo:bool, vector_matching:bool} } ================================================================================ ## COORDINATES (important) - Top-left origin, no Y-flip (same as Konva). - Place a box on YOUR rendering of the page: x = bbox_norm.x * yourPageWidth ; y = bbox_norm.y * yourPageHeight w = bbox_norm.width * yourPageWidth ; h = bbox_norm.height * yourPageHeight - bbox_norm is DPI-independent (recommended). bbox_px is at 200 DPI; each page reports width_px/height_px/render_dpi. - We rasterize PDFs at 200 DPI using the page CropBox + rotation. If your renderer uses MediaBox or different rotation, boxes can shift even at matching aspect ratio — render against the same box, or calibrate once on a shared PDF. - Konva: ================================================================================ ## CONFIDENCE - Persisted detections are bounded by inference thresholds at processing time (default 0.10; a single-class-per-job filter drops lone detections < 0.25). You cannot recover detections below those. - Display filtering (read-time): default = Massalaskuri's per-class thresholds (counts match our app's global active-symbol config); `?confidence=X` = flat floor. Re-filtering needs no reprocessing. - summary and returned detections always use the same filter (counts reconcile). ================================================================================ ## ERRORS Shape: {"error":{"code","message","details"?}}. Codes: unauthorized(401), not_found(404), conflict(409), unprocessable(422), rate_limited(429), upstream_error(502), internal_error(500). Rate limit: default 120 req/min per key → 429. Every response has an X-Request-Id header. ================================================================================ ## WEBHOOKS If callback_url is set, we POST the done payload (same shape as GET /v1/jobs/{id} done, minus inlined detections — fetch via pages[].detections_url with your X-API-Key). Header: X-Massalaskuri-Signature: t=,v1= where v1 = HMAC_SHA256(your_webhook_secret, "." + rawBody) Verify over the RAW body, length-safe constant-time compare, reject if |now - t| > 300s. Return 2xx to ack; we retry w/ backoff (≤6). callback_url must be https + public (private/loopback rejected at init). ================================================================================ ## VERSIONING Path-versioned (/v1). Additive changes don't bump the version — ignore unknown JSON fields. Breaking changes ship under /v2. ================================================================================ ## SUPPORT & SLA Email info@massalaskuri.fi for a key, higher rate limits, or incidents — reply within 1 business day. Pilot SLA: target uptime 99.9%; planned maintenance announced in advance. ================================================================================ ## LINKS - OpenAPI 3.1: https://api.massalaskuri.com/openapi.json - Interactive docs / try-it: https://api.massalaskuri.com/docs - Integration kit (sample PDF, bash/Node/Python, webhook receiver, typed SDK, Postman collection): https://api.massalaskuri.com/examples - Live visual demo: https://api.massalaskuri.com/playground - Status: https://api.massalaskuri.com/status