{"openapi":"3.1.0","info":{"title":"Pirros External API","version":"1.0.0","description":"OAuth 2.0 authenticated API for accessing Pirros project data.\n\n## Hosts\n\n| What | Host |\n|---|---|\n| Resource endpoints (`/v1/workspaces/*`) | `https://external-api.pirros.com` |\n| OAuth token / revoke (`/oauth/token`, `/oauth/revoke`) | `https://auth.pirros.com` |\n\nThe **Servers** dropdown, the **Authorize** dialog and **Try it out** on this page use these hosts. `/v1/workspaces/me` is part of the main Pirros application (logged-in user session, not a client-credentials token) and is only useful for looking up your workspace id.\n\n## Authentication\n\nThis API uses the OAuth 2.0 Client Credentials flow. Obtain an access token from the auth host, then send it as a Bearer token to every `/v1/workspaces/*` endpoint on the external API host.\n\n1. Get a token:\n```\ncurl -X POST https://auth.pirros.com/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>\"\n```\n\n2. Call the API with the returned `access_token`:\n```\ncurl https://external-api.pirros.com/v1/workspaces/<workspaceId>/tags \\\n  -H \"Authorization: Bearer <access_token>\"\n```\n\nExample header:\n```\nAuthorization: Bearer <your_access_token>\n```\n\n## Rate Limiting\n\nThe `/v1/workspaces/*` resource endpoints on the external API host are rate-limited to 1,000 requests per hour per client. Those endpoints include rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`) on all responses.\n\nWhen the rate limit is exceeded, the API returns a `429 Too Many Requests` error with a `Retry-After` header indicating when you can retry.\n\n`/oauth/token` (on the Pirros auth host) has its own, stricter limit: 5 attempts per 15 minutes per client_id + IP, returning `429` with error code `rate_limit_exceeded` and a `Retry-After` header; a successful token issuance resets the counter. It does not emit `RateLimit-*` headers.\n\n## Error Format\n\n`/v1/*` endpoints return a structured error body:\n```json\n{\n  \"error\": { \"code\": \"error_code\", \"message\": \"Human-readable description\" },\n  \"meta\": { \"request_id\": \"uuid-for-tracing\" }\n}\n```\nOn a `403 insufficient_scope` / `invalid_filter` the `error` object also carries a `details` block (granted filter ids/names, required action, entity type).\n\n`/oauth/*` endpoints return the flat OAuth 2.0 (RFC 6749) shape instead:\n```json\n{\n  \"error\": \"error_code\",\n  \"error_description\": \"Human-readable description\",\n  \"request_id\": \"uuid-for-tracing\"\n}\n```\n\nCommon error codes:\n- `invalid_request`: Missing or invalid request parameters (incl. unknown query parameter, bad cursor, bad UUID)\n- `invalid_client`: Client authentication failed\n- `invalid_token`: Bearer token is invalid or expired\n- `insufficient_scope`: Client lacks an eligible filter for the requested action/entity (LIST/CREATE)\n- `free_text_requires_broad_access`: A free-text family `?q=` search was attempted without a broad family grant (403)\n- `invalid_filter`: A write candidate (create/update) does not satisfy any granted write filter (403)\n- `not_found`: Resource not found. Also returned for GET/DOWNLOAD/UPDATE when no granted filter matches, and for cross-firm/cross-workspace access — existence is never leaked (404, not 403)\n- `access_denied`: Permission denied (generic forbidden fallback; rarely emitted)\n- `UNSUPPORTED_ALGOLIA_ATTRIBUTE`: A resource-filter attribute is not supported on the search/LIST path (e.g. detail `project_id`/`created_at`); 400\n- `INVALID_ITEM_REFERENCE`: A stash item references a detail/family not valid for the stash's project (400)\n- `IDEMPOTENCY_CONFLICT`: Same Idempotency-Key reused with a different request body (409)\n- `DOWNLOAD_DISABLED`: The resource is not available for download (403)\n- `NO_RVT_AVAILABLE` / `RFA_NOT_AVAILABLE`: No downloadable RVT/RFA artifact exists for the resource\n- `rate_limit_exceeded`: Per-client rate limit exceeded (429; the `/oauth/token` limiter uses this code)\n- `service_unavailable`: External API temporarily disabled by the operator kill switch (503)\n"},"servers":[{"url":"https://external-api.pirros.com","description":"Production — external API host (serves /v1/workspaces/*; /oauth/* is served by the Pirros auth host and /v1/workspaces/me by the main Pirros API host)"}],"components":{"securitySchemes":{"oauth2":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://auth.pirros.com/oauth/token","scopes":{}}}}},"schemas":{"TokenResponse":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer","exclusiveMinimum":0},"request_id":{"type":"string","format":"uuid"}},"required":["access_token","token_type","expires_in","request_id"],"description":"OAuth 2.0 access token response","example":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c","token_type":"Bearer","expires_in":3600,"request_id":"123e4567-e89b-12d3-a456-426614174000"}},"OAuthErrorResponse":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"},"request_id":{"type":"string","format":"uuid"}},"required":["error","request_id"],"description":"OAuth 2.0 error response (RFC 6749)","example":{"error":"invalid_client","error_description":"Client authentication failed","request_id":"123e4567-e89b-12d3-a456-426614174000"}},"TokenRequest":{"type":"object","properties":{"grant_type":{"type":"string","enum":["client_credentials"]},"client_id":{"type":"string","format":"uuid"},"client_secret":{"type":"string","minLength":1}},"required":["grant_type","client_id","client_secret"],"description":"OAuth 2.0 client credentials request"},"RevokeSuccess":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"],"description":"Token revocation acknowledgement (RFC 7009 idempotence)","example":{"request_id":"123e4567-e89b-12d3-a456-426614174000"}},"RevokeRequest":{"type":"object","properties":{"token":{"type":"string"},"client_id":{"type":"string","format":"uuid"},"client_secret":{"type":"string","minLength":1}},"required":["client_id","client_secret"],"description":"OAuth 2.0 token revocation request"},"ExternalTagDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"workspace_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","workspace_id","created_at"],"description":"Tag metadata for external API consumers","example":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","name":"structural","workspace_id":"550e8400-e29b-41d4-a716-446655440000","created_at":"2024-01-15T10:30:00Z"}},"ExternalTagListResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalTagDto"}},"next_cursor":{"type":"null"},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Workspace tag list (hard-capped at 500, no pagination)"},"V1ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","properties":{"required_action":{"type":"string"},"entity_type":{"type":"string"},"entity_id":{"type":["string","null"],"format":"uuid"},"granted_filter_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"granted_filter_names":{"type":"array","items":{"type":"string"}},"failed_predicate_filter_ids":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["required_action","entity_type","entity_id","granted_filter_ids","granted_filter_names"]}},"required":["code","message"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["error","meta"],"description":"Structured error response for /v1/* endpoints. error.details is present only on 403 insufficient_scope / invalid_filter.","example":{"error":{"code":"invalid_token","message":"Bearer token is invalid or expired"},"meta":{"request_id":"123e4567-e89b-12d3-a456-426614174000"}}},"ExternalDetailDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"project_id":{"type":"string","format":"uuid"},"view_name":{"type":["string","null"]},"title_on_sheet":{"type":["string","null"]},"detail_type":{"type":"string"},"detail_number":{"type":["string","null"]},"sheet_name":{"type":["string","null"]},"sheet_number":{"type":["string","null"]},"scale":{"type":["string","null"]},"pdf_url":{"type":"string"},"revit_version":{"type":["string","null"]},"has_2d_elements":{"type":"boolean"},"is_downloadable":{"type":"boolean"},"notes":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","project_id","view_name","title_on_sheet","detail_type","detail_number","sheet_name","sheet_number","scale","pdf_url","revit_version","has_2d_elements","is_downloadable","notes","created_at","updated_at"],"description":"Detail metadata for external API consumers","example":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","project_id":"550e8400-e29b-41d4-a716-446655440000","view_name":"Foundation Plan - Level 1","title_on_sheet":"Wall Section Detail","detail_type":"Section","detail_number":"3/A-101","sheet_name":"Architectural Plans","sheet_number":"A-101","scale":"1/4\" = 1'-0\"","pdf_url":"https://cdn.example.com/details/7c9e6679-7425-40de-944b-e07fc1f90ae7.pdf","revit_version":"2024","has_2d_elements":true,"is_downloadable":true,"notes":"Updated per latest code requirements","created_at":"2024-01-15T10:30:00Z","updated_at":"2024-01-16T14:22:00Z"}},"ExternalDetailListResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalDetailDto"}},"next_cursor":{"type":["string","null"]},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Cursor-paginated detail search results"},"ExternalDetailResponse":{"type":"object","properties":{"response":{"allOf":[{"$ref":"#/components/schemas/ExternalDetailDto"},{"type":"object","properties":{"expanded":{"type":"object","properties":{}}},"required":["expanded"]}]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Single detail resource"},"ExternalFamilySummaryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"classification":{"type":"string"},"revit_version":{"type":"string"}},"required":["id","name","category","classification","revit_version"],"description":"Slim family summary for the families list/search endpoint","example":{"id":"f1a8e3d2-9c4b-4f7e-8a6d-1c3b5e7d9a02","name":"Curtain-Wall-Type-A","category":"Doors","classification":"Architectural","revit_version":"2024"}},"ExternalFamilyListResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFamilySummaryDto"}},"next_cursor":{"type":["string","null"]},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Cursor-paginated families list/search results"},"ExternalFamilyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"current_version_id":{"type":"string","format":"uuid"},"version_number":{"type":"integer"},"name":{"type":"string"},"category":{"type":"string"},"parent_category":{"type":"string"},"classification":{"type":"string"},"revit_version":{"type":"string"},"project_id":{"type":"string","format":"uuid"},"notes":{"type":["string","null"]},"is_downloadable":{"type":"boolean"},"read_only":{"type":"boolean"},"status":{"type":"string","enum":["processing","ready","failed"]},"failure_reason":{"type":["string","null"]},"change_message":{"type":["string","null"]},"file_size":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","current_version_id","version_number","name","category","parent_category","classification","revit_version","project_id","notes","is_downloadable","read_only","status","failure_reason","change_message","file_size","created_at","updated_at"],"description":"Family metadata for external API consumers","example":{"id":"f1a8e3d2-9c4b-4f7e-8a6d-1c3b5e7d9a02","current_version_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","version_number":3,"name":"Curtain-Wall-Type-A","category":"Doors","parent_category":"Doors","classification":"Architectural","revit_version":"2024","project_id":"9f5b3c2d-1ea8-4f2c-8a61-d1c9e54b03f1","notes":"Updated mullion spacing","is_downloadable":true,"read_only":false,"status":"ready","failure_reason":null,"change_message":"Adjusted glazing thickness","file_size":"2458123","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-02-01T14:22:00Z"}},"ExternalFamilyResponse":{"type":"object","properties":{"response":{"allOf":[{"$ref":"#/components/schemas/ExternalFamilyDto"},{"type":"object","properties":{"expanded":{"type":"object","properties":{}}},"required":["expanded"]}]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Single family resource"},"ExternalSimilarFamilyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"version_number":{"type":"integer"},"name":{"type":"string"},"category":{"type":"string"},"parent_category":{"type":"string"},"classification":{"type":"string"},"revit_version":{"type":"string"},"project_id":{"type":"string","format":"uuid"},"similarity_score":{"type":"number"}},"required":["id","version_number","name","category","parent_category","classification","revit_version","project_id","similarity_score"],"description":"Similar-family result item (opaque similarity_score in [0,1])"},"ExternalSimilarFamilyResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalSimilarFamilyDto"}},"next_cursor":{"type":"null"},"similarity_version":{"type":"string"},"model_version":{"type":"string"},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","similarity_version","model_version","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Version-stamped similar families (hard-capped at 20, no pagination)"},"ExternalFamilyVersionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"family_id":{"type":"string","format":"uuid"},"version_number":{"type":"integer"},"is_current":{"type":"boolean"},"name":{"type":"string"},"category":{"type":"string"},"parent_category":{"type":"string"},"revit_version":{"type":"string"},"thumbnail_url":{"type":["string","null"]},"change_message":{"type":["string","null"]},"uploaded_by":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["user"]},"user_id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["type","user_id","name"]},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"}},"required":["id","family_id","version_number","is_current","name","category","parent_category","revit_version","thumbnail_url","change_message","uploaded_by","created_at"],"description":"A single family version in the history list. uploaded_by is null when the uploader is unknown; thumbnail_url is a Cloudinary public URL or null."},"ExternalFamilyHistoryListResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFamilyVersionDto"}},"next_cursor":{"type":["string","null"]},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Cursor-paginated family version history (newest first; excludes the original upload)"},"ExternalFamilyNestedResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFamilySummaryDto"}},"next_cursor":{"type":"null"},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Families the seed family directly nests, as referenced on its current version (full-return; not deduplicated; next_cursor always null in v2)"},"ExternalFamilyNestingResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFamilySummaryDto"}},"next_cursor":{"type":"null"},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Families that directly nest the seed family / where-used, as referenced on its current version (full-return; not deduplicated; next_cursor always null in v2)"},"ExternalStashDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"project_id":{"type":"string","format":"uuid"},"firm_id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"is_permanent":{"type":"boolean"},"is_firm_stash":{"type":"boolean"},"is_archived":{"type":"boolean"},"downloaded_at":{"type":["string","null"],"format":"date-time"},"items":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"item_type":{"type":"string","enum":["detail"]},"detail_id":{"type":"string","format":"uuid"},"family_id":{"type":"null"}},"required":["item_type","detail_id","family_id"]},{"type":"object","properties":{"item_type":{"type":"string","enum":["family"]},"detail_id":{"type":"null"},"family_id":{"type":"string","format":"uuid"}},"required":["item_type","detail_id","family_id"]}]}},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","description","project_id","firm_id","workspace_id","is_permanent","is_firm_stash","is_archived","downloaded_at","items","created_at"],"description":"Stash metadata for external API consumers. is_firm_stash is read-only: true for a firm stash, listed under Firm for every member whose role can view firm stashes; false for a stash listed in its owner's Private list. An API key's stash placement decides it at creation.","example":{"id":"8a3b1c4d-22de-4f9a-9e3a-77b88ad0e514","name":"Wall Sections - North Wing","description":"Curated wall sections for the schematic-design package","project_id":"9f5b3c2d-1ea8-4f2c-8a61-d1c9e54b03f1","firm_id":"f1a8e3d2-9c4b-4f7e-8a6d-1c3b5e7d9a02","workspace_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","is_permanent":true,"is_firm_stash":false,"is_archived":false,"downloaded_at":"2026-02-01T14:22:00Z","items":[{"item_type":"detail","detail_id":"b3a8f1c4-22de-4f9a-9e3a-77b88ad0e514","family_id":null},{"item_type":"family","detail_id":null,"family_id":"c4b9f2d5-33ef-450b-af4b-88c99be1f625"}],"created_at":"2026-01-15T10:30:00Z"}},"ExternalStashResponse":{"type":"object","properties":{"response":{"allOf":[{"$ref":"#/components/schemas/ExternalStashDto"},{"type":"object","properties":{"expanded":{"type":"object","properties":{}}},"required":["expanded"]}]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Single stash resource"},"ExternalStashItemListResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"item_type":{"type":"string","enum":["detail"]},"detail_id":{"type":"string","format":"uuid"},"family_id":{"type":"null"}},"required":["item_type","detail_id","family_id"]},{"type":"object","properties":{"item_type":{"type":"string","enum":["family"]},"detail_id":{"type":"null"},"family_id":{"type":"string","format":"uuid"}},"required":["item_type","detail_id","family_id"]}]}},"next_cursor":{"type":["string","null"]},"expanded":{"type":"object","properties":{}}},"required":["data","next_cursor","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"Cursor-paginated stash membership (lifts the 500-item cap of the stash GET)"},"ExternalStashCreateBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"project_id":{"type":"string","format":"uuid"},"item_ids":{"type":"object","properties":{"detail_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"family_ids":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["detail_ids","family_ids"]},"is_permanent":{"type":"boolean"}},"required":["name","project_id","item_ids"],"description":"Create a stash from detail and family references. Every referenced item must belong to project_id within the workspace; any cross-project/cross-workspace/missing reference returns a single generic 400 INVALID_ITEM_REFERENCE (no id disclosure)."},"ExternalStashUpdateBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"is_permanent":{"type":"boolean"}},"description":"Update a stash. Only name, description, and is_permanent are mutable; membership, project, and identity are not. At least one field is required."},"ExternalSignedDownloadResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"download_url":{"type":"string","format":"uri"},"expires_at":{"type":["string","null"],"format":"date-time"},"expanded":{"type":"object","properties":{}}},"required":["download_url","expires_at","expanded"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"A download URL for the resource. RVT and RFA return a 15-minute AWS S3 pre-signed URL with expires_at set; the detail PDF returns a Cloudinary delivery URL with expires_at null (the asset is not in S3 and the URL does not expire)."},"WorkspaceMeItem":{"type":"object","properties":{"workspace_id":{"type":"string","format":"uuid"},"workspace_name":{"type":"string"},"user_has_manage_oauth_clients":{"type":"boolean"},"user_has_manage_resource_filters":{"type":"boolean"},"user_has_create_firm_stashes":{"type":"boolean"},"archived":{"type":"boolean"}},"required":["workspace_id","workspace_name","user_has_manage_oauth_clients","user_has_manage_resource_filters","archived"],"description":"One workspace the caller can access, annotated with the per-workspace permission booleans (manage OAuth clients / manage resource filters / create firm stashes) used by the Scope Selector, the Filter Management page and the stash placement setting. user_has_create_firm_stashes is absent from a response served before it shipped.","example":{"workspace_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","workspace_name":"Structural West Coast","user_has_manage_oauth_clients":true,"user_has_manage_resource_filters":false,"user_has_create_firm_stashes":true,"archived":false}},"ListWorkspacesMeResponse":{"type":"object","properties":{"response":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMeItem"}},"next_cursor":{"type":"null"}},"required":["data","next_cursor"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["response","meta"],"description":"The caller's accessible non-archived workspaces. Unpaginated in v1 (next_cursor is always null)."},"MainApiFlatErrorResponse":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"requestId":{"type":"string"}},"required":["error"],"description":"Flat error body returned by the main Pirros API host (which serves /v1/workspaces/me) — not the structured /v1 envelope used by the external API host.","example":{"error":"Missing or invalid authorization header","requestId":"123e4567-e89b-12d3-a456-426614174000"}}},"parameters":{}},"paths":{"/oauth/token":{"post":{"tags":["Authentication"],"summary":"Obtain access token","description":"Exchange client credentials for a Bearer token. The returned access token must be included in the Authorization header for the /v1/workspaces/* resource endpoints on the external API host. It is NOT used by the main-API-host endpoint /v1/workspaces/me, which requires a logged-in user session instead. Served by the Pirros auth host (not the external API host); the access token is a 1-hour, non-refreshable JWT for the client_credentials grant.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Access token granted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Invalid request (missing or invalid fields)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthErrorResponse"}}}},"401":{"description":"Invalid client credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthErrorResponse"}}}},"429":{"description":"Rate limit exceeded (5 attempts per 15 minutes per client_id + IP, error code rate_limit_exceeded, Retry-After header set; a successful token issuance resets the counter)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthErrorResponse"}}}}}},"servers":[{"url":"https://auth.pirros.com","description":"Pirros auth host (serves /oauth/*)"}]},"/oauth/revoke":{"post":{"tags":["Authentication"],"summary":"Revoke access token (v1 no-op)","description":"No-op in v1. This endpoint authenticates the client and always returns 200 per RFC 7009 idempotence, but revokes nothing — it does not invalidate any issued token. Actual credential revocation — which instantly invalidates the tokens a credential has issued — is performed by a firm administrator through the Pirros credential-management UI. Served by the Pirros auth host (not the external API host).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}},"responses":{"200":{"description":"Acknowledged (client authenticated; nothing revoked)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeSuccess"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthErrorResponse"}}}},"401":{"description":"Invalid client credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthErrorResponse"}}}}}},"servers":[{"url":"https://auth.pirros.com","description":"Pirros auth host (serves /oauth/*)"}]},"/v1/workspaces/{workspaceId}/tags":{"get":{"tags":["Tags"],"summary":"List workspace tags","description":"List the tags in a workspace the credential can read. Hard-capped at 500, ordered by name; the list is not paginated (next_cursor is always null).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"}],"responses":{"200":{"description":"Tag list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalTagListResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Forbidden (no resource filter grants access to this list)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/details":{"get":{"tags":["Details"],"summary":"Search workspace details","description":"Search details in a workspace. Customer filters (detail_type, tag, revit_version, q) narrow the result and AND-combine with the credential authorization filters. `revit_version` matches the detail’s available-version set via the `availableRevitVersions` facet and is normalized to a 4-digit year. Cursor-paginated; the cursor is opaque and resets to the first page when the query filters change. (Date filtering on details — created_after/created_before — is deferred past v1: the Algolia details index has no numeric createdAt to range-filter.) An unrecognized revit_version (no standalone 4-digit year) matches nothing and returns an empty result — it is not rejected. Authorization-filter attribute support is path-dependent: a detail resource filter on project_id is honored on the GET-by-id path but rejected with 400 (UNSUPPORTED_ALGOLIA_ATTRIBUTE) on this search path, and created_at is unsupported on both.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"detail_type","in":"query"},{"schema":{"type":"string"},"required":false,"name":"tag","in":"query"},{"schema":{"type":"string"},"required":false,"name":"revit_version","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Detail search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalDetailListResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Forbidden (no resource filter grants access to this list)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/details/{id}":{"get":{"tags":["Details"],"summary":"Get a workspace detail by ID","description":"Retrieve a single detail scoped to the workspace. A detail in another workspace, an archived/missing detail, and a detail no granted filter matches all return 404 (indistinguishable — no existence disclosure).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Detail found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalDetailResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Detail not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families":{"get":{"tags":["Families"],"summary":"List or search workspace families","description":"List families in a workspace. Without `q`, structured filters (tag, category, revit_version) run in SQL and the result is keyset-paginated by (created_at, id). With `q`, a free-text Algolia search runs (requires a broad family grant, else 403 free_text_requires_broad_access) with the same structured filters applied as facets. The cursor is opaque. With `q`, a cursor reused under a changed query resets to the first page. Without `q`, the cursor encodes ordering position only and continues from that position under the new filters; re-paginate from the start when filters change. `revit_version` matches a family’s available-version set (authored or successfully upgraded) and is normalized to a 4-digit year. An unrecognized revit_version (no standalone 4-digit year) matches nothing and returns an empty result — it is not rejected.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"tag","in":"query"},{"schema":{"type":"string"},"required":false,"name":"category","in":"query"},{"schema":{"type":"string"},"required":false,"name":"revit_version","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Families list/search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFamilyListResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Forbidden — either no resource filter grants access to this list (insufficient_scope), or a free-text `?q=` search was attempted with a narrow (non-broad) family grant (free_text_requires_broad_access). Free-text search requires a broad family grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families/{id}":{"get":{"tags":["Families"],"summary":"Get a workspace family by ID","description":"Retrieve a single family (current version) scoped to the workspace. A family in another workspace, an archived/missing family, and a family no granted filter matches all return 404 (indistinguishable — no existence disclosure).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Family found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFamilyResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Family not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families/{id}/similar":{"get":{"tags":["Families"],"summary":"List families similar to a seed family","description":"Return up to 20 families similar to the seed family, ranked by the version-stamped similarity model and scoped to the workspace. The seed family is resolved like the family GET (a cross-firm or missing seed returns 404). limit is 1-20 (default 10) and is rejected, never clamped, when out of range; the response is not paginated (next_cursor is always null). When the seed has no similar families, the response is an empty list with 200 (not 404).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Similar families","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSimilarFamilyResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Seed family not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families/{id}/history":{"get":{"tags":["Families"],"summary":"List a family’s version history","description":"Return the seed family’s version history (versions after the original upload), newest first, keyset-paginated by (created_at, id). The seed family is resolved like the family GET — a cross-workspace, cross-firm, missing, or unmatched seed returns 404 (no existence disclosure). A family with no revisions returns an empty list.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Family version history","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFamilyHistoryListResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Seed family not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families/{id}/nested":{"get":{"tags":["Families"],"summary":"List families the seed family nests","description":"Return the families the seed family directly nests (its one-hop children / what’s inside it), as recorded on the seed’s current version. Each entry is the referenced version’s summary — not re-resolved to that family’s current version. Mirrors the internal family-references view: not deduplicated (one entry per reference) and unordered. The seed family is resolved like the family GET (cross-workspace / cross-firm / missing / unmatched -> 404). Full-return: next_cursor is always null and ?cursor/?limit are rejected (reserved).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Nested families","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFamilyNestedResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Seed family not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families/{id}/nesting":{"get":{"tags":["Families"],"summary":"List families that nest the seed family","description":"Return the families that directly nest the seed family (where it is used), as recorded on the seed’s current version. Each entry is the referenced (parent) version’s summary — not re-resolved to that family’s current version. Mirrors the internal family-references view: not deduplicated (one entry per reference) and unordered. The seed family is resolved like the family GET (cross-workspace / cross-firm / missing / unmatched -> 404). Full-return: next_cursor is always null and ?cursor/?limit are rejected (reserved).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Nesting families (where-used)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFamilyNestingResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Seed family not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/stashes/{id}":{"get":{"tags":["Stashes"],"summary":"Get a workspace stash by ID","description":"Retrieve a single stash scoped to the workspace, including its flat membership list (details and families, hard-capped at 500). project_id is derived from the stash items and firm_id from the workspace. A stash in another workspace, a cross-firm stash, and a missing stash all return 404 (indistinguishable — no existence disclosure).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Stash found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalStashResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Stash not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}},"patch":{"tags":["Stashes"],"summary":"Update a stash","description":"Update a stash's name, description, or is_permanent flag. Accepts an optional Idempotency-Key header. The post-update stash must satisfy a granted stash:write filter. A stash in another workspace or missing returns 404 (no existence disclosure).","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalStashUpdateBody"}}}},"responses":{"200":{"description":"Stash updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalStashResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"The post-update stash does not satisfy a granted stash:write filter (invalid_filter)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Stash not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"409":{"description":"Idempotency-Key replay conflict (IDEMPOTENCY_CONFLICT): the same Idempotency-Key was reused with a different request body. An identical retry instead replays the original stored response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/stashes/{id}/items":{"get":{"tags":["Stashes"],"summary":"List a stash’s items","description":"Return the stash’s membership (details and families), keyset-paginated by (sort_order, id) — lifting the 500-item cap of the embedded list in the stash GET. The parent stash is resolved like the stash GET: a cross-workspace, cross-firm, or missing stash returns 404 (no existence disclosure). An empty stash returns an empty list.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Stash items","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalStashItemListResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Stash not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/stashes":{"post":{"tags":["Stashes"],"summary":"Create a stash","description":"Create a workspace-scoped stash. Accepts an optional Idempotency-Key header for safe retries. The candidate stash must satisfy a granted stash:write filter (403 invalid_filter otherwise); an invalid item reference returns 400 INVALID_ITEM_REFERENCE. The response equals a subsequent GET. Ownership: a stash created with an API key is owned by the member who created the key, and one created through a connected AI client by the member who connected it. By default it appears in that member's Private stash list. An API key whose stash placement a firm administrator set to \"firm\" creates a firm stash instead, listed under Firm for every member whose role can view firm stashes; while the key's creator lacks the Create Firm Stashes permission in the workspace the request is refused with 403 FIRM_STASH_NOT_PERMITTED, and no private stash is made in its place. is_firm_stash in the response says which it is.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalStashCreateBody"}}}},"responses":{"201":{"description":"Stash created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalStashResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Token has no eligible stash:write filter, or the candidate does not satisfy one (insufficient_scope / invalid_filter); the API key's creator is no longer an active member of its firm (CREDENTIAL_OWNER_UNAVAILABLE); or the key places its stashes with the firm and its creator lacks Create Firm Stashes in this workspace (FIRM_STASH_NOT_PERMITTED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"409":{"description":"Idempotency-Key replay conflict (IDEMPOTENCY_CONFLICT): the same Idempotency-Key was reused with a different request body. An identical retry instead replays the original stored response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/downloads/pdf/{detailId}":{"get":{"tags":["Downloads"],"summary":"Get a detail PDF download URL","description":"Return a download URL for a detail's current PDF rendering. Detail PDFs are Cloudinary assets, so download_url is a public Cloudinary delivery URL and expires_at is null. Requires a detail:download filter. A cross-workspace/unmatched detail returns 404 (no existence disclosure); a non-downloadable detail returns 403 DOWNLOAD_DISABLED.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"detailId","in":"path"}],"responses":{"200":{"description":"Download URL issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSignedDownloadResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Detail is not downloadable (DOWNLOAD_DISABLED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Detail not found in this workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/downloads/rvt/{detailId}":{"get":{"tags":["Downloads"],"summary":"Get a detail RVT download URL","description":"Return a 15-minute AWS S3 pre-signed URL for a detail's current RVT file. Requires a detail:download filter. A cross-workspace/unmatched detail returns 404; a non-downloadable detail returns 403 DOWNLOAD_DISABLED; a detail whose RVT file is absent returns 404 NO_RVT_AVAILABLE.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"detailId","in":"path"}],"responses":{"200":{"description":"Signed URL issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSignedDownloadResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Detail is not downloadable (DOWNLOAD_DISABLED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Detail not found in this workspace, or no RVT file is available (NO_RVT_AVAILABLE)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/{workspaceId}/families/{id}/rfa":{"get":{"tags":["Downloads"],"summary":"Get a family RFA download URL","description":"Return a 15-minute AWS S3 pre-signed URL for a family's current RFA file. Requires a family:download filter. A cross-workspace/unmatched family returns 404; a non-downloadable family returns 403 DOWNLOAD_DISABLED; a family with no current version returns 404 RFA_NOT_AVAILABLE.","security":[{"oauth2":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"workspaceId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Signed URL issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalSignedDownloadResponse"}}}},"400":{"description":"Invalid request (bad path param, query param, or cursor)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"401":{"description":"Unauthorized (invalid or missing token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"403":{"description":"Family is not downloadable (DOWNLOAD_DISABLED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"404":{"description":"Family not found in this workspace, or no current RFA version is available (RFA_NOT_AVAILABLE)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (1000 requests per hour per client)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}},"503":{"description":"External API temporarily disabled by operator kill switch (service_unavailable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/V1ErrorResponse"}}}}}}},"/v1/workspaces/me":{"get":{"tags":["Workspaces"],"summary":"List the caller's accessible workspaces","description":"Return the current user's accessible non-archived workspaces, each annotated with the two per-workspace management permission booleans (manage OAuth clients / manage resource filters) used by the Scope Selector and Filter Management page. Served by the main Pirros API host and requires a logged-in user session (not a client-credentials Bearer token). Unpaginated in v1 (next_cursor is always null).","responses":{"200":{"description":"Accessible workspaces","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspacesMeResponse"}}}},"401":{"description":"Unauthorized (no logged-in user session). Flat main-API-host error shape, not the /v1 envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MainApiFlatErrorResponse"}}}}}},"servers":[{"url":"http://localhost:3000","description":"Pirros main API host (serves /v1/workspaces/me under a logged-in user session)"}]}},"webhooks":{}}