openapi: 3.2.0 info: title: Operations Hub Core.projects API version: 0.1.1 description: '' servers: [] tags: - name: core.projects paths: /api/core/v1/projects: get: operationId: core_endpoints_projects_list_projects summary: List Projects parameters: - in: query name: search schema: anyOf: - type: string - type: 'null' description: Case-insensitive match on project code or names of linked locations (active project–location links only). SOL-number lookup lives in GET /core/v1/projects/search. Each project is returned at most once. title: Search required: false description: Case-insensitive match on project code or names of linked locations (active project–location links only). SOL-number lookup lives in GET /core/v1/projects/search. Each project is returned at most once. - in: query name: sort schema: anyOf: - type: string - type: 'null' description: Sort by field (prefix with - for descending) title: Sort required: false description: Sort by field (prefix with - for descending) - in: query name: requesting_customer_id schema: anyOf: - type: integer - type: 'null' description: Filter by requesting customer ID (ignored when requesting_customer_ids is set) title: Requesting Customer Id required: false description: Filter by requesting customer ID (ignored when requesting_customer_ids is set) - in: query name: billing_customer_id schema: anyOf: - type: integer - type: 'null' description: Filter by billing customer ID (ignored when billing_customer_ids is set) title: Billing Customer Id required: false description: Filter by billing customer ID (ignored when billing_customer_ids is set) - in: query name: vendor_id schema: anyOf: - type: integer - type: 'null' description: Filter by vendor ID (ignored when vendor_ids is set) title: Vendor Id required: false description: Filter by vendor ID (ignored when vendor_ids is set) - in: query name: country_id schema: anyOf: - type: integer - type: 'null' description: Filter by country ID (ignored when country_ids is set) title: Country Id required: false description: Filter by country ID (ignored when country_ids is set) - in: query name: status_id schema: anyOf: - type: integer - type: 'null' description: Filter by status ID (ignored when status_ids is set) title: Status Id required: false description: Filter by status ID (ignored when status_ids is set) - in: query name: project_manager_id schema: anyOf: - type: integer - type: 'null' description: Filter by project manager ID (ignored when project_manager_ids is set) title: Project Manager Id required: false description: Filter by project manager ID (ignored when project_manager_ids is set) - in: query name: salesperson_id schema: anyOf: - type: integer - type: 'null' description: Filter by salesperson ID (ignored when salesperson_ids is set) title: Salesperson Id required: false description: Filter by salesperson ID (ignored when salesperson_ids is set) - in: query name: type schema: anyOf: - type: string - type: 'null' description: Filter by project type (REGULAR, WARRANTY, DEMO, RND, ACADEMY, or INTERNAL) title: Type required: false description: Filter by project type (REGULAR, WARRANTY, DEMO, RND, ACADEMY, or INTERNAL) - in: query name: types schema: anyOf: - type: string - type: 'null' description: Comma-separated project types (OR within), e.g. REGULAR,WARRANTY. Empty/omitted = no type filter. When set, type is ignored. title: Types required: false description: Comma-separated project types (OR within), e.g. REGULAR,WARRANTY. Empty/omitted = no type filter. When set, type is ignored. - in: query name: status_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated status IDs (OR within). Empty/omitted = no status filter. When set, status_id is ignored. title: Status Ids required: false description: Comma-separated status IDs (OR within). Empty/omitted = no status filter. When set, status_id is ignored. - in: query name: country_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated country IDs (OR within). Empty/omitted = no country filter. When set, country_id is ignored. title: Country Ids required: false description: Comma-separated country IDs (OR within). Empty/omitted = no country filter. When set, country_id is ignored. - in: query name: requesting_customer_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated requesting customer IDs (OR within). Empty/omitted = no filter on this facet. When set, requesting_customer_id is ignored. title: Requesting Customer Ids required: false description: Comma-separated requesting customer IDs (OR within). Empty/omitted = no filter on this facet. When set, requesting_customer_id is ignored. - in: query name: billing_customer_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated billing customer IDs (OR within). Empty/omitted = no filter on this facet. When set, billing_customer_id is ignored. title: Billing Customer Ids required: false description: Comma-separated billing customer IDs (OR within). Empty/omitted = no filter on this facet. When set, billing_customer_id is ignored. - in: query name: vendor_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated vendor IDs (OR within). Empty/omitted = no vendor filter. When set, vendor_id is ignored. title: Vendor Ids required: false description: Comma-separated vendor IDs (OR within). Empty/omitted = no vendor filter. When set, vendor_id is ignored. - in: query name: project_manager_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated project manager user IDs (OR within). Empty/omitted = no filter on this facet. When set, project_manager_id is ignored. title: Project Manager Ids required: false description: Comma-separated project manager user IDs (OR within). Empty/omitted = no filter on this facet. When set, project_manager_id is ignored. - in: query name: salesperson_ids schema: anyOf: - type: string - type: 'null' description: Comma-separated salesperson user IDs (OR within). Empty/omitted = no filter on this facet. When set, salesperson_id is ignored. title: Salesperson Ids required: false description: Comma-separated salesperson user IDs (OR within). Empty/omitted = no filter on this facet. When set, salesperson_id is ignored. - in: query name: scope schema: anyOf: - type: string - type: 'null' description: '''mine'' or ''all''' title: Scope required: false description: '''mine'' or ''all''' - in: query name: paginate schema: default: true description: Enable pagination (false returns all results) title: Paginate type: boolean required: false description: Enable pagination (false returns all results) - in: query name: page schema: default: 1 description: Page number minimum: 1 title: Page type: integer required: false description: Page number - in: query name: page_size schema: default: 50 description: Number of items per page maximum: 1000 minimum: 1 title: Page Size type: integer required: false description: Number of items per page responses: '200': description: OK content: application/json: schema: items: $ref: '#/components/schemas/ProjectResponse' title: Response type: array '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' description: 'Return a paginated list of projects, filterable by query parameters. Pure technician users see only projects where they are assigned as a resource (via workorder assignments or project resource sets). Admin users bypass this scope.' tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] post: operationId: core_endpoints_projects_create_project summary: Create Project parameters: [] responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectResponse' description: Create a new project. project_builder_id is optional for natively created projects. tags: - core.projects requestBody: content: application/json: schema: $ref: '#/components/schemas/ProjectCreate' required: true security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/search: get: operationId: core_endpoints_projects_search_projects summary: Search Projects parameters: - in: query name: search schema: default: '' title: Search type: string required: false responses: '200': description: OK content: application/json: schema: items: $ref: '#/components/schemas/ProjectSearchResult' title: Response type: array description: 'Typed search for the global header search box, projects listed first. Returns project rows (matched on project code or an active linked location name) followed by SOL rows (matched on SOL number), each group capped at 10. Matching is token-prefix and unicode-aware: the term must start the value or follow a separator, so ``550`` finds ``VEUS-5502`` but not ``VEUS-3550``. A blank ``search``, or one shorter than three characters after stripping leading separators, returns an empty list. Deliberately requires ``regular_user`` only — the same key that unlocks ``get_project`` — so anyone who can search a project can also open it (``technician_user`` alone can list but not open, the mismatch behind the old "visible in the table, 403 on open" bug). Technician-group members holding ``regular_user`` still only see their assigned projects, matching the project list scope.' tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}: get: operationId: core_endpoints_projects_get_project summary: Get Project parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectResponse' description: Retrieve a single project by its numeric ID or UUID. tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] patch: operationId: core_endpoints_projects_update_project summary: Update Project parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectResponse' description: Partially update a project. The project_builder_id link cannot be removed. tags: - core.projects requestBody: content: application/json: schema: $ref: '#/components/schemas/ProjectUpdate' required: true security: - APIKeyAuth: [] - CookieAuth: [] delete: operationId: core_endpoints_projects_delete_project summary: Delete Project parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Success' description: Soft-delete a project, marking it as inactive. tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/overview: get: operationId: core_endpoints_projects_get_project_overview summary: Get Project Overview parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectOverviewResponse' description: 'Decoupled project overview card (meta, scope, deployments, dates). Fields already on the base project response (code, country, PM, salesperson, status, LPS standard, description, customers) are intentionally omitted — read those from ``GET /core/v1/projects/{id}``.' tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] patch: operationId: core_endpoints_projects_update_project_overview summary: Update Project Overview parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectOverviewResponse' description: 'Update overview fields across Core (description, LPS) and legacy tables. Does not reuse the legacy ``update_overview_info``; writes each backing table directly via ORM in a single transaction.' tags: - core.projects requestBody: content: application/json: schema: $ref: '#/components/schemas/ProjectOverviewEdit' required: true security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/overview/financials: get: operationId: core_endpoints_projects_get_project_overview_financials summary: Get Project Overview Financials parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectOverviewFinancialsResponse' description: Decoupled project financial overview (CO/charged/PO values, offer status). tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/status-history: get: operationId: core_endpoints_projects_get_project_status_history summary: Get Project Status History parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: items: $ref: '#/components/schemas/ProjectStatusHistoryEntry' title: Response type: array description: 'Chronological history of a project''s status transitions (oldest first). Each entry records the status moved from/to, the trigger (``change_reason``), an optional note, when it happened, and who caused it. History accrues from when tracking was introduced — projects with no recorded transitions yet return an empty list.' tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/validate: get: operationId: core_endpoints_projects_validate_project summary: Validate Project parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectValidateResponse' description: Validate project configuration and return a list of warning codes. tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/refresh-google-drive: post: operationId: core_endpoints_projects_refresh_project_google_drive summary: Refresh Project Google Drive parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Success' description: 'Provision Google Drive folders for a project if the ROOT link is not yet set. Skips silently when the ROOT link already exists. Uses the Pipedrive deal URL when available, then falls back to auto-discovery if it is missing or invalid.' tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/complete: post: operationId: core_endpoints_projects_mark_project_completed summary: Mark Project Completed parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectResponse' description: 'Manually transition a project from Closure to Completed. The cascade only advances projects up to Closure (all SOLs Completed or Cancelled). Moving to Completed is an explicit close-out action — there is no automatic trigger.' tags: - core.projects security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/status-override: post: operationId: core_endpoints_projects_override_project_status summary: Override Project Status parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectResponse' description: 'Manually set a project''s status, bypassing the stage gates. Privileged override gated behind the ``force_complete_project`` permission. Any status except Cancelled can be set, in either direction, including reopening a Completed project. Rules, audit trail, and the deliberate no-side-effects contract live in ``ProjectManager.override_status``. The automatic status rules (SOL cascade, nightly reconciler) stay active and may later move the status again.' tags: - core.projects requestBody: content: application/json: schema: $ref: '#/components/schemas/ProjectStatusOverride' required: true security: - APIKeyAuth: [] - CookieAuth: [] /api/core/v1/projects/{project_id}/pre-job-received: put: operationId: core_endpoints_projects_update_project_pre_job_received summary: Update Project Pre Job Received parameters: - in: path name: project_id schema: title: Project Id type: string required: true responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProjectResponse' description: 'Set or clear the customer pre-job questionnaire ''received'' timestamp. This is the Core replacement for the legacy ``PUT /projects/{id}/user-status`` pre-job workflow path. Only the *null-ness* of ``payload.pre_job_received_at`` is read — it signals intent; the timestamp itself is stamped **server-side** (``timezone.now()``) to stay robust against client clock skew: - ``null -> non-null`` (customer **submit**): stamps the timestamp, sends the pre-job submission notification, and re-runs the Pipedrive status cascade (advancing the project to Planning when probability is 100). - ``non-null -> null`` (staff **reopen**): clears the timestamp; no email, no cascade. - already-received re-submit: keeps the existing timestamp; no side effects.' tags: - core.projects requestBody: content: application/json: schema: $ref: '#/components/schemas/ProjectPreJobReceivedUpdate' required: true security: - APIKeyAuth: [] - CookieAuth: [] components: schemas: CountryWithRegionObject: additionalProperties: false description: CountryObject extended with region — only for responses that prefetch country__region. properties: id: description: Country ID title: Id type: integer code: description: Country code (ISO 3166-1 alpha-2) title: Code type: string name: description: Country name title: Name type: string region: anyOf: - type: string - type: 'null' description: Region name (nullable) title: Region required: - id - code - name title: CountryWithRegionObject type: object ProjectStatusOverride: additionalProperties: false description: Payload for the manual status-override action endpoint. properties: status_code: description: Target ProjectStatus code (e.g. 'Planning'). Matched case-insensitively against the configured statuses; 'Cancelled' is refused — cancellation has its own flow. title: Status Code type: string reason: description: Why the status is overridden. Required non-blank; stored on the status change log and shown in the status history. maxLength: 1000 title: Reason type: string required: - status_code - reason title: ProjectStatusOverride type: object ProjectOverviewResponse: additionalProperties: false description: Decoupled project overview payload (Core/dispatch-sourced). properties: updated_at: description: Last update timestamp format: date-time title: Updated At type: string project_folder_link: anyOf: - type: string - type: 'null' description: Google Drive ROOT folder URL (core.ProjectGoogleDriveLink) title: Project Folder Link report_recipients: description: Report recipients (core.ProjectContact) items: $ref: '#/components/schemas/ProjectOverviewReportRecipient' title: Report Recipients type: array dedicated_contact_for_scope_change: anyOf: - type: string - type: 'null' description: Pre-job answer (core.ProjectPreJobAnswers) title: Dedicated Contact For Scope Change is_lps_project: default: false description: Whether the scope includes any LPS service title: Is Lps Project type: boolean robotic_sets: description: Planned resource set names items: $ref: '#/components/schemas/ProjectOverviewRoboticSet' title: Robotic Sets type: array categories: description: Service categories present in scope items: type: string title: Categories type: array service_full_codes: description: Service codes present in scope items: type: string title: Service Full Codes type: array blades: default: 0 description: Distinct blade-level scope items title: Blades type: integer turbines: default: 0 description: Distinct turbines in scope title: Turbines type: integer total_units: default: 0 description: blades + turbines title: Total Units type: integer rat_needed: default: false description: Whether any service code requires RAT title: Rat Needed type: boolean sites: description: Project sites items: $ref: '#/components/schemas/ProjectOverviewSite' title: Sites type: array reinspection: anyOf: - type: boolean - type: 'null' description: Reinspection flag (legacy projects.PipedriveDeal) title: Reinspection rope_access_contract: anyOf: - type: string - type: 'null' description: Rope access contract link (legacy projects.ProjectOverviewInfo) title: Rope Access Contract project_status_dashboard: anyOf: - type: string - type: 'null' description: Project status dashboard link (legacy projects.ProjectOverviewInfo) title: Project Status Dashboard planned_start_date: anyOf: - type: string - type: 'null' description: MIN(planned resource set item start_date) title: Planned Start Date planned_end_date: anyOf: - type: string - type: 'null' description: MAX(planned resource set item end_date) title: Planned End Date actual_start_date: anyOf: - type: string - type: 'null' description: Earliest non-cancelled workorder activity start title: Actual Start Date actual_end_date: anyOf: - type: string - type: 'null' description: Latest completed workorder activity end title: Actual End Date planned_duration: anyOf: - type: string - type: 'null' description: Human-readable planned duration title: Planned Duration required: - updated_at title: ProjectOverviewResponse type: object ProjectStatusHistoryEntry: additionalProperties: false description: A single project status transition (for the overview status timeline). properties: old_status: anyOf: - type: string - type: 'null' description: Status the project moved from (null for the first entry) title: Old Status new_status: anyOf: - type: string - type: 'null' description: Status the project moved to title: New Status change_reason: anyOf: - type: string - type: 'null' description: Trigger that caused the change (e.g. create, pipedrive, sol_cascade, completion_check, manual, reconcile). title: Change Reason change_note: anyOf: - type: string - type: 'null' description: Optional free-text note about the change title: Change Note status_changed_at: anyOf: - type: string - type: 'null' description: When the transition occurred (ISO 8601) title: Status Changed At changed_by: anyOf: - type: string - type: 'null' description: Name or email of the user who caused the change, if known title: Changed By title: ProjectStatusHistoryEntry type: object ProjectPreJobReceivedUpdate: additionalProperties: false description: 'Payload for the pre-job-received action endpoint. Only the *null-ness* of ``pre_job_received_at`` is read: a non-null value marks the questionnaire as submitted (and locks it), ``null`` reopens it. On submit the timestamp is stamped server-side, so any non-null value sent here is treated purely as the "submitted" signal — its value is ignored.' properties: pre_job_received_at: anyOf: - format: date-time type: string - type: 'null' description: Submit/reopen signal. Non-null = submitted (locks the pre-job questionnaire; the stored timestamp is stamped server-side); null = reopened (unlocks it). title: Pre Job Received At required: - pre_job_received_at title: ProjectPreJobReceivedUpdate type: object ProjectCreate: additionalProperties: false description: Schema for creating a Project. properties: code: description: Unique project code minLength: 1 title: Code type: string type: anyOf: - type: string - type: 'null' description: 'Project type: REGULAR, DEMO, RND, or ACADEMY. Defaults to REGULAR when omitted. WARRANTY and INTERNAL cannot be set via the API.' title: Type description: anyOf: - type: string - type: 'null' description: Project description title: Description requesting_customer_id: anyOf: - type: integer - type: 'null' description: Requesting customer ID title: Requesting Customer Id billing_customer_id: anyOf: - type: integer - type: 'null' description: Billing customer ID title: Billing Customer Id vendor_id: anyOf: - type: integer - type: 'null' description: Vendor ID title: Vendor Id country_id: anyOf: - type: integer - type: 'null' description: Country ID title: Country Id timezone_id: anyOf: - type: integer - type: 'null' description: Timezone ID title: Timezone Id status_id: anyOf: - type: integer - type: 'null' description: Project status ID title: Status Id project_manager_id: anyOf: - type: integer - type: 'null' description: Project manager user ID title: Project Manager Id salesperson_id: anyOf: - type: integer - type: 'null' description: Salesperson user ID title: Salesperson Id pipedrive_id: anyOf: - type: integer - type: 'null' description: Pipedrive deal ID title: Pipedrive Id pipedrive_probability: anyOf: - maximum: 100 minimum: 0 type: integer - type: 'null' description: 'Deal probability (0-100). Sending it here is a manual override, exactly as it is on PATCH: the value is marked sticky so the Pipedrive deals-refresh poll no longer overwrites it. Unlike PATCH this does not re-run the status cascade, because a project that has just been created has no internally-approved commercial offer and so cannot advance past Upcoming whatever the probability says. Omit it to leave Pipedrive in control.' title: Pipedrive Probability project_builder_id: anyOf: - format: uuid type: string - type: 'null' description: Project builder project UUID (legacy) - optional for natively created projects title: Project Builder Id planned_start_date: anyOf: - type: string - type: 'null' description: Planned project start date title: Planned Start Date planned_end_date: anyOf: - type: string - type: 'null' description: Planned project end date title: Planned End Date actual_start_date: anyOf: - type: string - type: 'null' description: Actual project start date title: Actual Start Date actual_end_date: anyOf: - type: string - type: 'null' description: Actual project end date title: Actual End Date expected_close_date: anyOf: - type: string - type: 'null' description: 'When sales expects the deal to be decided. Owned by the sales form: it writes this on deal creation and again whenever the date moves on its pipeline board, so a later change there wins over a hand-edit made in Ops Hub. Commercial only: no cascade or schedule reads it.' title: Expected Close Date lps_standard_id: anyOf: - type: integer - type: 'null' description: ID of the LPS report template/standard (core.standard) title: Lps Standard Id required: - code title: ProjectCreate type: object TimezoneObject: additionalProperties: false description: Nested timezone object for responses (no timestamps in nested objects). properties: id: description: Timezone ID title: Id type: integer timezone: description: Timezone name (e.g., Europe/Riga) title: Timezone type: string city: anyOf: - type: string - type: 'null' description: City name title: City utc_offset: anyOf: - type: string - type: 'null' description: UTC offset (e.g., +02:00) title: Utc Offset required: - id - timezone title: TimezoneObject type: object Success: additionalProperties: false description: 'Schema returned for successful operations. The `success` field is always ``true`` in this schema. Failed operations are represented by the :class:`Error` schema instead, so a ``false`` value does not occur in practice. The field is included for consistency across responses and to make the contract explicit for clients.' properties: success: default: true description: Always true for this schema. Errors are represented by a separate Error schema, so false is never returned. title: Success type: boolean title: Success type: object ErrorCode: description: Error codes for API errors. enum: - validation - server - auth - unknown - external - generic title: ErrorCode type: string UserObject: additionalProperties: false description: Nested user object for responses (no timestamps). properties: id: title: Id type: integer username: title: Username type: string email: anyOf: - type: string - type: 'null' title: Email first_name: anyOf: - type: string - type: 'null' title: First Name last_name: anyOf: - type: string - type: 'null' title: Last Name required: - id - username title: UserObject type: object ProjectOverviewFinancialsResponse: additionalProperties: false description: Decoupled project financial overview (Core + accounting). properties: updated_at: description: Last update timestamp format: date-time title: Updated At type: string co_value: anyOf: - type: number - type: 'null' description: Approved offer total (Sum of items.amount) title: Co Value charged_value: anyOf: - type: number - type: 'null' description: Charged value (NetSuite service charges) title: Charged Value purchase_order_value: anyOf: - type: number - type: 'null' description: Sum of purchase order version amounts title: Purchase Order Value actual_units_done: anyOf: - type: integer - type: 'null' description: Distinct charged top-level scope items title: Actual Units Done currency: anyOf: - type: string - type: 'null' description: Billing currency code title: Currency offer_status: anyOf: - type: string - type: 'null' description: Status of the latest project offer title: Offer Status gross_margin: anyOf: - type: string - type: 'null' description: Gross margin note (legacy projects.ProjectOverviewInfo) title: Gross Margin required: - updated_at title: ProjectOverviewFinancialsResponse type: object StandardObject: additionalProperties: false description: Nested ``core.standard`` object for responses (LPS / HVT templates). properties: id: title: Id type: integer name: title: Name type: string type: title: Type type: string key: title: Key type: string required: - id - name - type - key title: StandardObject type: object WarrantyCounterpartProjectObject: additionalProperties: false description: 'The project on the other side of a warranty relationship (no timestamps). Direction-neutral: the source project (on a WARRANTY project) or the warranty project (on a REGULAR project). ``type`` lets the client tell which without inferring from the current project''s type.' properties: id: description: Counterpart project ID title: Id type: integer code: description: Counterpart project code title: Code type: string type: description: Counterpart project type title: Type type: string project_builder_id: anyOf: - format: uuid type: string - type: 'null' description: Counterpart project builder UUID (for frontend routing) title: Project Builder Id required: - id - code - type title: WarrantyCounterpartProjectObject type: object ProjectStatusObject: additionalProperties: false description: Nested project status object for responses (no timestamps). properties: id: title: Id type: integer code: title: Code type: string required: - id - code title: ProjectStatusObject type: object VendorObject: additionalProperties: false description: Nested vendor object for responses (no timestamps). properties: id: title: Id type: integer number: anyOf: - type: string - type: 'null' title: Number display_name: title: Display Name type: string required: - id - display_name title: VendorObject type: object ProjectValidateResponse: additionalProperties: false description: Response schema for project validation endpoint. properties: warnings: items: type: string title: Warnings type: array required: - warnings title: ProjectValidateResponse type: object ProjectLocationSummary: additionalProperties: false description: Summary of sites (locations) linked to a project for list/detail payloads. properties: primary_name: anyOf: - type: string - type: 'null' description: Display name of the first linked site when ordered by project_location id ascending title: Primary Name total_count: description: Number of active project–site links minimum: 0 title: Total Count type: integer all_names: anyOf: - items: type: string type: array - type: 'null' description: All site names when total_count > 1; omitted or null when absent or unnecessary title: All Names required: - total_count title: ProjectLocationSummary type: object ProjectOverviewRoboticSet: additionalProperties: false description: Planned resource set name shown on the project overview. properties: name: description: Planned resource set name title: Name type: string required: - name title: ProjectOverviewRoboticSet type: object ProjectSearchResult: additionalProperties: false description: 'One global-search hit: a project row or a service-order-line (SOL) row. Project rows match on project code or an active linked location name; SOL rows match on the SOL number. Both row kinds point at a project, so ``project_id`` / ``project_builder_id`` always identify the navigation target.' properties: type: description: 'Kind of hit: a project itself or one of its SOLs' enum: - project - sol title: Type type: string project_id: description: Numeric Core project ID title: Project Id type: integer project_code: description: Project code title: Project Code type: string project_builder_id: anyOf: - format: uuid type: string - type: 'null' description: Project builder project UUID (legacy) title: Project Builder Id customer_name: anyOf: - type: string - type: 'null' description: Requesting customer name of the project title: Customer Name matched_location_name: anyOf: - type: string - type: 'null' description: Name of the linked site the term matched; set on project rows only, and only when a site (not just the code) matched title: Matched Location Name sol_id: anyOf: - type: integer - type: 'null' description: ServiceOrderLine ID; set on sol rows only title: Sol Id sol_number: anyOf: - type: string - type: 'null' description: SOL number; set on sol rows only title: Sol Number required: - type - project_id - project_code title: ProjectSearchResult type: object CustomerObject: additionalProperties: false description: Nested customer object for responses (no timestamps). properties: id: title: Id type: integer number: anyOf: - type: string - type: 'null' title: Number display_name: title: Display Name type: string netsuite_customer_id: anyOf: - type: integer - type: 'null' description: NetSuite customer ID title: Netsuite Customer Id currency: anyOf: - $ref: '#/components/schemas/CurrencyIdCodeObject' - type: 'null' description: Customer currency (id and code only) required: - id - display_name title: CustomerObject type: object CurrencyIdCodeObject: additionalProperties: false description: Nested currency object with only id and code (no timestamps). properties: id: title: Id type: integer code: title: Code type: string required: - id - code title: CurrencyIdCodeObject type: object Error: additionalProperties: false description: Error response schema. properties: code: $ref: '#/components/schemas/ErrorCode' message: title: Message type: string required: - code - message title: Error type: object ProjectResponse: additionalProperties: false description: Schema for Project response. properties: updated_at: description: Last update timestamp format: date-time title: Updated At type: string id: description: Project ID title: Id type: integer code: description: Unique project code title: Code type: string type: description: 'Project type: REGULAR, WARRANTY, DEMO, RND, ACADEMY, or INTERNAL' title: Type type: string description: anyOf: - type: string - type: 'null' description: Project description title: Description is_offshore: default: false description: 'Read-only, auto-derived: true iff any active project location points to a location with windpark_type=''Offshore''. Kept in sync by core.signals; not writable through this API.' title: Is Offshore type: boolean requesting_customer: anyOf: - $ref: '#/components/schemas/CustomerObject' - type: 'null' description: Requesting customer billing_customer: anyOf: - $ref: '#/components/schemas/CustomerObject' - type: 'null' description: Billing customer vendor: anyOf: - $ref: '#/components/schemas/VendorObject' - type: 'null' description: Vendor country: anyOf: - $ref: '#/components/schemas/CountryWithRegionObject' - type: 'null' description: Country timezone: anyOf: - $ref: '#/components/schemas/TimezoneObject' - type: 'null' description: Timezone status: anyOf: - $ref: '#/components/schemas/ProjectStatusObject' - type: 'null' description: Project status project_manager: anyOf: - $ref: '#/components/schemas/UserObject' - type: 'null' description: Project manager salesperson: anyOf: - $ref: '#/components/schemas/UserObject' - type: 'null' description: Salesperson pipedrive_id: anyOf: - type: integer - type: 'null' description: Pipedrive deal ID title: Pipedrive Id pipedrive_probability: anyOf: - type: integer - type: 'null' description: Latest deal probability (0-100). Mirrored from Pipedrive during the deals-refresh poll unless probability_manually_overridden is true, in which case it holds the value set by a manual RFI edit. title: Pipedrive Probability probability_manually_overridden: default: false description: True when pipedrive_probability was set by a manual RFI edit and is therefore sticky against the Pipedrive deals-refresh poll. Cleared when the probability is set to null via the RFI form. title: Probability Manually Overridden type: boolean pre_job_received_at: anyOf: - type: string - type: 'null' description: Timestamp when the customer pre-job questionnaire was received (set on submit via PUT /projects/{id}/pre-job-received). Non-null means the questionnaire is submitted/locked; null means it is still editable. title: Pre Job Received At project_builder_id: anyOf: - format: uuid type: string - type: 'null' description: Project builder project UUID (legacy) title: Project Builder Id planned_start_date: anyOf: - type: string - type: 'null' description: Planned project start date title: Planned Start Date planned_end_date: anyOf: - type: string - type: 'null' description: Planned project end date title: Planned End Date actual_start_date: anyOf: - type: string - type: 'null' description: Actual project start date title: Actual Start Date actual_end_date: anyOf: - type: string - type: 'null' description: Actual project end date title: Actual End Date expected_close_date: anyOf: - type: string - type: 'null' description: 'When sales expects the deal to be decided. Owned by the sales form: it writes this on deal creation and again whenever the date moves on its pipeline board, so a later change there wins over a hand-edit made in Ops Hub. Commercial only: no cascade or schedule reads it.' title: Expected Close Date potential_start_date: anyOf: - type: string - type: 'null' description: 'Potential project start date: the soonest start_date across the project''s planned resource sets (MIN over ProjectPlannedResourceSet items). Distinct from planned_start_date (Wrike). Null when the project has no planned set with a start date.' title: Potential Start Date potential_end_date: anyOf: - type: string - type: 'null' description: 'Potential project end date: the latest end_date across the project''s planned resource sets (MAX over ProjectPlannedResourceSet items). Distinct from planned_end_date (Wrike). Null when the project has no planned set with an end date.' title: Potential End Date lps_standard: anyOf: - $ref: '#/components/schemas/StandardObject' - type: 'null' description: Selected LPS report template/standard (passed to the reporting engine as lps_standard_id). Null when not yet picked or for non-LPS projects. scope_change_auto_accept_enabled: default: false description: When true, scope change requests on this project are auto-approved and finalized immediately on creation (legacy behaviour). When false, requests enter the QC → Sales → PM approval flow. title: Scope Change Auto Accept Enabled type: boolean location_summary: $ref: '#/components/schemas/ProjectLocationSummary' description: Linked sites summary; primary follows ascending project_location id billing_currency: anyOf: - type: string - type: 'null' description: Currency code from the project's billing record (e.g. EUR, USD). title: Billing Currency warranty_counterpart_project: anyOf: - $ref: '#/components/schemas/WarrantyCounterpartProjectObject' - type: 'null' description: 'The project on the other side of the warranty relationship: the source project (on a WARRANTY project) or the warranty project (on a REGULAR project). Null when there is no linkage.' required: - updated_at - id - code - type - location_summary title: ProjectResponse type: object ProjectUpdate: additionalProperties: false description: Schema for updating a Project. properties: code: anyOf: - minLength: 1 type: string - type: 'null' description: Unique project code title: Code description: anyOf: - type: string - type: 'null' description: Project description title: Description requesting_customer_id: anyOf: - type: integer - type: 'null' description: Requesting customer ID title: Requesting Customer Id billing_customer_id: anyOf: - type: integer - type: 'null' description: Billing customer ID title: Billing Customer Id vendor_id: anyOf: - type: integer - type: 'null' description: Vendor ID title: Vendor Id country_id: anyOf: - type: integer - type: 'null' description: Country ID title: Country Id timezone_id: anyOf: - type: integer - type: 'null' description: Timezone ID title: Timezone Id status_id: anyOf: - type: integer - type: 'null' description: Project status ID title: Status Id project_manager_id: anyOf: - type: integer - type: 'null' description: Project manager user ID title: Project Manager Id salesperson_id: anyOf: - type: integer - type: 'null' description: Salesperson user ID title: Salesperson Id pipedrive_id: anyOf: - type: integer - type: 'null' description: Pipedrive deal ID title: Pipedrive Id pipedrive_probability: anyOf: - maximum: 100 minimum: 0 type: integer - type: 'null' description: 'Deal probability (0-100). Editing it here is a manual RFI override: it drives the master-planning status cascade (Upcoming->Initiation at >=90, Initiation->Planning at 100), re-runs the status recompute, and marks the value sticky so the Pipedrive deals-refresh poll no longer overwrites it. Setting it to null clears the override and hands control back to Pipedrive.' title: Pipedrive Probability project_builder_id: anyOf: - format: uuid type: string - type: 'null' description: Project builder project UUID (legacy) title: Project Builder Id planned_start_date: anyOf: - type: string - type: 'null' description: Planned project start date title: Planned Start Date planned_end_date: anyOf: - type: string - type: 'null' description: Planned project end date title: Planned End Date actual_start_date: anyOf: - type: string - type: 'null' description: Actual project start date title: Actual Start Date actual_end_date: anyOf: - type: string - type: 'null' description: Actual project end date title: Actual End Date expected_close_date: anyOf: - type: string - type: 'null' description: 'When sales expects the deal to be decided. Owned by the sales form: it writes this on deal creation and again whenever the date moves on its pipeline board, so a later change there wins over a hand-edit made in Ops Hub. Commercial only: no cascade or schedule reads it.' title: Expected Close Date lps_standard_id: anyOf: - type: integer - type: 'null' description: ID of the LPS report template/standard (core.standard) title: Lps Standard Id pre_job_received_at: anyOf: - format: date-time type: string - type: 'null' description: Customer pre-job questionnaire 'received' timestamp. Non-null locks the questionnaire; null reopens it. Writing it through this generic update is a plain persist with no side effects — prefer the PUT /projects/{id}/pre-job-received action endpoint, which also sends the submission email and re-runs the status cascade on submit. title: Pre Job Received At scope_change_auto_accept_enabled: anyOf: - type: boolean - type: 'null' description: 'When true, scope change requests on this project are auto-approved and finalized immediately on creation; when false they enter the QC → Sales → PM review flow. Field-level permission: including this field in the body requires auth.project_manager — 403 otherwise.' title: Scope Change Auto Accept Enabled title: ProjectUpdate type: object ProjectOverviewSite: additionalProperties: false description: A project site with coordinates for the overview list. properties: name: anyOf: - type: string - type: 'null' description: Location name title: Name latitude: anyOf: - type: number - type: 'null' description: Latitude title: Latitude longitude: anyOf: - type: number - type: 'null' description: Longitude title: Longitude title: ProjectOverviewSite type: object ProjectOverviewEdit: additionalProperties: false description: 'Editable project overview fields. Writes span Core + legacy tables. Only explicitly-sent keys are applied (``exclude_unset=True``).' properties: project_description: anyOf: - type: string - type: 'null' description: Writes core.Project.description title: Project Description lps_standard_id: anyOf: - type: integer - type: 'null' description: Writes core.Project.lps_standard_id title: Lps Standard Id rope_access_contract: anyOf: - type: string - type: 'null' description: Writes legacy projects.ProjectOverviewInfo title: Rope Access Contract project_status_dashboard: anyOf: - type: string - type: 'null' description: Writes legacy projects.ProjectOverviewInfo title: Project Status Dashboard gross_margin: anyOf: - type: string - type: 'null' description: Writes legacy projects.ProjectOverviewInfo title: Gross Margin reinspection: anyOf: - type: boolean - type: 'null' description: Writes legacy projects.PipedriveDeal (+ BusinessTravelOrder mirror) title: Reinspection title: ProjectOverviewEdit type: object ProjectOverviewReportRecipient: additionalProperties: false description: Report recipient (customer contact) shown on the project overview. properties: name: anyOf: - type: string - type: 'null' description: Contact name title: Name email: anyOf: - type: string - type: 'null' description: Contact email title: Email title: ProjectOverviewReportRecipient type: object securitySchemes: APIKeyAuth: type: http scheme: bearer CookieAuth: type: apiKey in: cookie name: opshub_prod_sessionid AuthBearer: type: http scheme: bearer