{"openapi":"3.1.0","info":{"title":"Portravo project API","version":"1.0.1","description":"Scoped access to owned projects, first-plan upload, paid takeoff extraction, and reviewed estimate exports. Human line review, pricing, and finalization stay in Portravo. Availability is controlled by the deployment's external-API rollout flag; this document alone does not establish live access."},"servers":[{"url":"https://portravo.com/api/v1"}],"security":[{"PortravoKey":[]}],"components":{"schemas":{"ApiError":{"type":"object","additionalProperties":false,"properties":{"error":{"type":"object","additionalProperties":false,"properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]},"ProjectOperationError":{"type":"object","additionalProperties":false,"properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error"]},"Metro":{"type":"object","additionalProperties":false,"properties":{"code":{"type":"string"},"name":{"type":"string"},"state":{"type":"string"},"costIndex":{"type":"number"},"hasDirectPrices":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}},"required":["code","name","state","costIndex","hasDirectPrices","createdAt"]},"MetroList":{"type":"object","additionalProperties":false,"properties":{"metros":{"type":"array","items":{"$ref":"#/components/schemas/Metro"}}},"required":["metros"]},"ProjectSummary":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","description":"Owned project ID. Use this value in subsequent project routes."},"name":{"type":"string"},"address":{"type":["string","null"]},"status":{"type":"string","enum":["draft","extracting","review","priced","final","failed"]},"metroCode":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","address","status","metroCode","updatedAt"]},"ProjectList":{"type":"object","additionalProperties":false,"properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/ProjectSummary"},"maxItems":50},"nextOffset":{"type":["integer","null"],"minimum":0,"description":"Pass as offset for the next page; null means no further page."}},"required":["projects","nextOffset"]},"ProjectCreated":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","description":"Owned project ID. Use this value in subsequent project routes."},"status":{"type":"string","const":"draft"},"reviewUrl":{"type":"string","format":"uri","description":"Open in Portravo for human review and pricing."}},"required":["id","status","reviewUrl"]},"ProjectDetail":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","description":"Owned project ID. Use this value in subsequent project routes."},"name":{"type":"string"},"address":{"type":["string","null"]},"status":{"type":"string","enum":["draft","extracting","review","priced","final","failed"]},"metroCode":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"chargedAt":{"type":["string","null"],"format":"date-time","description":"When this project consumed an estimate credit; null before a successful charge."}},"required":["id","name","address","status","metroCode","updatedAt","chargedAt"]},"TakeoffLine":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string"},"page":{"type":["integer","null"]},"trade":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"qty":{"type":"number"},"unit":{"type":"string"},"confidence":{"type":"number"},"status":{"type":"string","enum":["pending","confirmed","edited","rejected"]},"assemblyId":{"type":["string","null"]},"notes":{"type":["string","null"]}},"required":["id","page","trade","category","description","qty","unit","confidence","status","assemblyId","notes"]},"PlanFileMetadata":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string"},"filename":{"type":"string"},"sizeBytes":{"type":"integer"},"contentType":{"type":"string"},"pageCount":{"type":["integer","null"]}},"required":["id","filename","sizeBytes","contentType","pageCount"]},"OwnedTakeoff":{"type":"object","additionalProperties":false,"properties":{"project":{"$ref":"#/components/schemas/ProjectDetail"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/TakeoffLine"}},"files":{"type":"array","items":{"$ref":"#/components/schemas/PlanFileMetadata"}},"metro":{"anyOf":[{"$ref":"#/components/schemas/Metro"},{"type":"null"}]},"reviewRequired":{"type":"boolean","description":"True when at least one takeoff line is pending. False alone does not establish pricing readiness or permit export."},"reviewUrl":{"type":"string","format":"uri"}},"required":["project","lines","files","metro","reviewRequired","reviewUrl"]},"PlanUploadGrant":{"type":"object","additionalProperties":false,"properties":{"uploadId":{"type":"string","description":"Use as uploadId in the completion route; also becomes planFileId after attachment."},"pathname":{"type":"string","description":"Exact private storage path bound to this grant and its clientToken."},"clientToken":{"type":"string","description":"Secret, short-lived upload capability for this grant's path only. Send it as Authorization: Bearer <clientToken> on the PUT to uploadUrl. Keep out of prompts, logs and screenshots. It is not the Portravo API key."},"expiresAt":{"type":"string","format":"date-time","description":"Grant expiry, fifteen minutes after issuance or renewal."},"maximumSizeInBytes":{"type":"integer","const":10485760},"contentType":{"type":"string","const":"application/pdf"},"access":{"type":"string","const":"private"},"uploadUrl":{"type":"string","format":"uri","description":"PUT the PDF bytes here (Content-Type: application/pdf, Authorization: Bearer <clientToken>). The response body is { url, pathname, size }."},"completionUrl":{"type":"string","format":"uri","description":"After the upload, POST its returned url here using the Portravo bearer key."}},"required":["uploadId","pathname","clientToken","expiresAt","maximumSizeInBytes","contentType","access","uploadUrl","completionUrl"]},"PlanAttached":{"type":"object","additionalProperties":false,"properties":{"planFileId":{"type":"string"},"status":{"type":"string","const":"attached"}},"required":["planFileId","status"]},"ExtractionResult":{"type":"object","additionalProperties":false,"properties":{"ok":{"type":"boolean","const":true},"lines":{"type":"integer","minimum":0,"description":"Number of extracted lines; read getOwnedTakeoff to retrieve the lines themselves."},"sheets":{"type":"array","items":{"type":"string"},"description":"Sheet labels seen in the plan, not a sheet count."},"scopeNotes":{"type":"string"}},"required":["ok","lines","sheets","scopeNotes"]}},"securitySchemes":{"UploadToken":{"type":"http","scheme":"bearer","bearerFormat":"ptrv_up_ upload token","description":"The clientToken from one upload grant. Valid only for that grant's uploadUrl until expiresAt."},"PortravoKey":{"type":"http","scheme":"bearer","bearerFormat":"ptrv_ API key","description":"Create a 90-day scoped key in Portravo Settings > Integrations. Save it in the client's secure credential store. Keys can be revoked in Settings."}}},"paths":{"/metros":{"get":{"operationId":"listPricingMetros","summary":"List supported pricing markets","description":"Requires projects:read.","responses":{"200":{"description":"Supported metros, pricing indexes and direct-price availability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetroList"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects":{"get":{"operationId":"listOwnedProjects","summary":"List your projects","description":"Requires projects:read. Returns up to 50 projects and nextOffset, ordered newest first.","parameters":[{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":999999,"default":0}}],"responses":{"200":{"description":"Owned projects and the next page offset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectList"}}}},"400":{"description":"Offset must be an integer from 0 to 999999.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"operationId":"createOwnedProject","summary":"Create a project for a new plan","description":"Requires projects:write. Creates a draft owned by the key's account. Does not consume an estimate credit.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9_.:-]+$"},"description":"Identity is scoped to the owner and operation. Reuse with identical input on retry, including after a timeout. Completed success or failure replays its original status/body. Changed input returns 409; unresolved requests return 409 and must not be automatically executed with a new key."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","maxLength":120,"minLength":1},"address":{"type":"string","maxLength":200},"metroCode":{"type":"string","maxLength":40,"minLength":1}},"required":["name"]}}}},"responses":{"201":{"description":"Project identifier, draft state and human review URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectCreated"}}}},"400":{"description":"Invalid JSON, fields, or operation input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Idempotency key input conflict, or the original operation is still running/unresolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"JSON body exceeds 16 KiB. Upload PDFs through a private upload grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}":{"get":{"operationId":"getOwnedTakeoff","summary":"Read your project's takeoff and review state","description":"Requires projects:read. File metadata excludes private blob URLs. Raw model output, billing identifiers and estimate items are excluded. Nullable fields remain present as null.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."}],"responses":{"200":{"description":"Owned project, takeoff lines, file metadata, market and human review handoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnedTakeoff"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Project not found for this key owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/uploads":{"post":{"operationId":"createInitialPlanUpload","summary":"Issue a private upload grant for the first plan","description":"Requires plans:write. Supports PDFs up to 10 MiB. Returns a 15-minute path-limited upload token. PUT the PDF bytes to uploadUrl with Content-Type: application/pdf and Authorization: Bearer <clientToken>, then call completeInitialPlanUpload with the url that upload returns. One durable grant per project; same-name retries renew it. Cannot replace a plan or upload to demo/reviewed/finalized projects. This is a JSON grant endpoint, not a binary upload endpoint. Already-issued grants expire normally even if the API key is revoked.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"filename":{"type":"string","maxLength":200,"minLength":5,"pattern":"^[^/\\\\\\x00-\\x1f]+\\.[pP][dD][fF]$"}},"required":["filename"]}}}},"responses":{"201":{"description":"Private path-bound capability and completion URL. Treat clientToken as a secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanUploadGrant"}}}},"400":{"description":"Invalid JSON, fields, or operation input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Project not found for this key owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Existing plan/review work is protected or the durable upload grant has a different filename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"JSON body exceeds 16 KiB. Upload PDFs through a private upload grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/uploads/{uploadId}/file":{"put":{"operationId":"uploadInitialPlanFile","summary":"Upload the plan PDF for an upload grant","description":"Authenticated by the grant's clientToken (Authorization: Bearer <clientToken>), not the API key. Body is the PDF bytes with Content-Type: application/pdf, at most 10 MiB. Writes once to the grant's private path; a second upload for the same grant returns 409. Returns the url to send to completeInitialPlanUpload.","security":[{"UploadToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."},{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"},"description":"uploadId from the private upload grant."}],"requestBody":{"required":true,"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Stored private file reference.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string"},"pathname":{"type":"string"},"size":{"type":"integer"}},"required":["url","pathname","size"]}}}},"400":{"description":"The body is not a PDF.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, wrong or expired clientToken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"A file was already uploaded for this grant, or the grant is complete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"410":{"description":"Upload grant expired; request a renewed grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"PDF larger than 10 MiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Send Content-Type: application/pdf.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/uploads/{uploadId}/complete":{"post":{"operationId":"completeInitialPlanUpload","summary":"Validate and attach the uploaded plan","description":"Requires plans:write. Re-checks owner, project, grant path, private store metadata, size/type and PDF signature. Exact retries are safe. No arbitrary URL is fetched. The PDF stays in Portravo's private store and follows existing extraction processing.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."},{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"},"description":"uploadId from the private upload grant."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","description":"The exact private file url returned by the upload, not a local path or arbitrary remote PDF URL."}},"required":["url"]}}}},"responses":{"200":{"description":"Attached plan identifier and state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanAttached"}}}},"400":{"description":"Invalid JSON, fields, or operation input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Upload not found for this owner and project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Completion conflicts with an existing plan or protected project state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"410":{"description":"Upload grant expired; request a renewed grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"JSON body exceeds 16 KiB. Upload PDFs through a private upload grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/extract":{"post":{"operationId":"extractInitialTakeoff","summary":"Run the project's initial paid takeoff","description":"Requires extractions:write and explicit confirmCreditUse:true after the user's request to spend an estimate credit. Uses the existing account balance, atomic reservation and failure refund logic. Processes steps for a bounded time per request; a long plan set returns 202 and continues through advanceExtraction. Initial API extractions are limited to 3 per 10 minutes per account. Extraction never confirms lines, prices, finalizes, or submits bids. A reviewed takeoff cannot be overwritten through the API. Read getOwnedTakeoff for actual lines and reviewUrl; human review is required.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9_.:-]+$"},"description":"Identity is scoped to the owner and operation. Reuse with identical input on retry, including after a timeout. Completed success or failure replays its original status/body. Changed input returns 409; unresolved requests return 409 and must not be automatically executed with a new key."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"confirmCreditUse":{"type":"boolean","const":true,"description":"Only send true after the user authorizes using existing estimate balance for this plan."}},"required":["confirmCreditUse"]}}}},"responses":{"200":{"description":"Extraction finished: counts, sheet labels and scope notes; no line approval or pricing occurs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResult"}}}},"202":{"description":"Extraction is still running (staged: inventory, one call per sheet, counts, aggregate). The body carries runId, state, step progress, retryAfterMs and advanceUrl. Continue with advanceExtraction using a NEW Idempotency-Key per advance; replaying this key returns this recorded response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResult"}}}},"400":{"description":"Invalid request, missing Idempotency-Key, or missing/invalid plan PDF.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/ProjectOperationError"}]}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"402":{"description":"Insufficient existing estimate balance. Use normal Portravo billing; this API never purchases credits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectOperationError"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Project not found for this key owner.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/ProjectOperationError"}]}}}},"409":{"description":"Protected review work, conflicting/running extraction, changed idempotency input, or unresolved original request.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/ProjectOperationError"}]}}}},"413":{"description":"JSON body exceeds 16 KiB. Upload PDFs through a private upload grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"500":{"description":"Extraction failed; the shared service applies its reservation/refund behavior. Inspect the project before a new extraction.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectOperationError"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/extract/advance":{"post":{"operationId":"advanceExtraction","summary":"Continue a running takeoff extraction","description":"Requires extractions:write. Processes the next steps of the run started by extractInitialTakeoff under a lease; never charges again. Send a NEW Idempotency-Key for every advance (a replayed key returns the recorded response, not new progress). Safe to call concurrently: a held lease answers 202 with busy:true. Wait retryAfterMs between calls. Limited to 60 per 10 minutes per account.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9_.:-]+$"},"description":"Identity is scoped to the owner and operation. Reuse with identical input on retry, including after a timeout. Completed success or failure replays its original status/body. Changed input returns 409; unresolved requests return 409 and must not be automatically executed with a new key."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"runId":{"type":"string","description":"runId from the extractInitialTakeoff 202 response."}},"required":["runId"]}}}},"responses":{"200":{"description":"Extraction finished.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResult"}}}},"202":{"description":"Still running; call again after retryAfterMs with a new Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResult"}}}},"400":{"description":"Invalid JSON, fields, or operation input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Run not found for this project and key owner.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/ProjectOperationError"}]}}}},"413":{"description":"JSON body exceeds 16 KiB. Upload PDFs through a private upload grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"Content-Type must be application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"500":{"description":"Extraction failed; the reserved estimate was refunded once. Inspect the project before a new extraction.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectOperationError"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/extract/status":{"get":{"operationId":"getExtractionStatus","summary":"Read the latest extraction run's status","description":"Requires projects:read. Read-only: never advances, leases or retries a run. Optional runId query selects a specific run.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."},{"name":"runId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Run status: state, step progress, coverage summary when done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractionResult"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No extraction run for this project and key owner.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/ProjectOperationError"}]}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/projects/{id}/export":{"get":{"operationId":"downloadReviewedEstimate","summary":"Download an owned, reviewed estimate workbook","description":"Requires estimates:read. Requires priced/final state and passing the existing line/mapping readiness checks. Includes quantity, material/labor pricing and source/location/as-of provenance. Human pricing and finalization occur in Portravo. Success is binary XLSX, not JSON or a signed download URL.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"An owned Portravo project ID."}],"responses":{"200":{"description":"Reviewed Excel workbook","headers":{"Content-Disposition":{"description":"Attachment filename for the workbook.","schema":{"type":"string"}}},"content":{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing, invalid, expired or revoked Portravo API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"WWW-Authenticate":{"description":"Bearer authentication challenge.","schema":{"type":"string"}}}},"403":{"description":"Required scope is missing or account onboarding is incomplete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Project not found for this key owner.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/ProjectOperationError"}]}}}},"409":{"description":"Review every line and price the estimate in Portravo before export.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Account request or operation allowance reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying; respect the longer operation window as well.","schema":{"type":"string","pattern":"^[0-9]+$"}}}},"503":{"description":"External API unavailable or service request failed. Preserve the original idempotency key when retrying an operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}}}