{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/cogram-com/json-schema/cogram-com-project-v1-schema.json",
  "title": "ProjectV1",
  "description": "Project response.",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/api.cogram.com-v1-openapi.json#/components/schemas/ProjectV1",
  "properties": {
    "id": {
      "type": "string",
      "title": "Id"
    },
    "name": {
      "type": "string",
      "title": "Name"
    },
    "description": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Description"
    },
    "external_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "External Id",
      "description": "Your system's ID for this project."
    },
    "client_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Client Name"
    },
    "project_address": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Project Address"
    },
    "is_active": {
      "type": "boolean",
      "title": "Is Active",
      "description": "False once the project is archived."
    },
    "email_filing_enabled": {
      "type": "boolean",
      "title": "Email Filing Enabled"
    },
    "meeting_filing_enabled": {
      "type": "boolean",
      "title": "Meeting Filing Enabled",
      "description": "Whether Cogram suggests this project for meetings and, where the organization allows it, files meetings into it. Only takes effect while the organization has meeting filing turned on. Filing a meeting here by hand works either way."
    },
    "is_public": {
      "type": "boolean",
      "title": "Is Public",
      "description": "If true, project appears in org project directory; non-members can request access."
    },
    "unanet_project_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Unanet Project Id",
      "description": "Unanet ERP A/E project id, for projects synced from Unanet; null for every other project. Stable across customer re-codes, so join on this rather than on external_id."
    },
    "unanet_lead_stage": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "title": "Unanet Lead Stage",
      "description": "Unanet lead/pursuit stage name (e.g. \"06-Submitted Proposal\"). Set means an open opportunity; null means a normal project."
    },
    "unanet_custom_fields": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "title": "Unanet Custom Fields",
      "description": "The project's Unanet user-defined fields, keyed by Unanet fieldName. Replaced in full on each sync, so a cleared field disappears."
    },
    "unanet_market_sectors": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/ProjectMarketSectorV1"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "title": "Unanet Market Sectors",
      "description": "The project's Unanet market sectors and their percentage split, in Unanet's order. Null for projects that aren't synced from Unanet or have no sectors."
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "title": "Created At"
    },
    "updated_at": {
      "anyOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "type": "null"
        }
      ],
      "title": "Updated At",
      "description": "Tracks the project record only — not the emails, RFIs, meetings or other records filed under it. Null if never updated."
    },
    "related_counts": {
      "anyOf": [
        {
          "$ref": "#/$defs/ProjectRelatedCountsV1"
        },
        {
          "type": "null"
        }
      ],
      "description": "Per-type counts of records filed under this project. Present only when the request asked for it with ?include=counts; null otherwise, so existing callers pay nothing for it."
    },
    "members": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/ProjectMemberSummaryV1"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "title": "Members",
      "description": "Users holding a role on this project, ordered by email. Present only when the request asked for it with ?include=members; null otherwise, so existing callers pay nothing for it. An empty list means the project genuinely has no members — distinguishable from null, which means they were not requested. Group-granted access is not listed; only direct per-user roles are."
    }
  },
  "type": "object",
  "required": [
    "id",
    "name",
    "description",
    "external_id",
    "client_name",
    "project_address",
    "is_active",
    "email_filing_enabled",
    "meeting_filing_enabled",
    "is_public",
    "unanet_project_id",
    "unanet_lead_stage",
    "unanet_custom_fields",
    "unanet_market_sectors",
    "created_at",
    "updated_at"
  ],
  "$defs": {
    "ProjectMarketSectorV1": {
      "properties": {
        "name": {
          "type": "string",
          "title": "Name",
          "description": "Sector name as configured in Unanet, e.g. \"Healthcare\"."
        },
        "pct": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "title": "Pct",
          "description": "Percent of the project attributed to this sector."
        }
      },
      "type": "object",
      "required": [
        "name"
      ],
      "title": "ProjectMarketSectorV1",
      "description": "One Unanet market sector on a project, with the share of the project\nattributed to it. Reported exactly as Unanet holds it — Cogram neither\nnormalizes the percentages nor requires them to add up."
    },
    "ProjectMemberRoleV1": {
      "type": "string",
      "enum": [
        "MEMBER",
        "OWNER",
        "LEAD",
        "VIEWER"
      ],
      "title": "ProjectMemberRoleV1",
      "description": "Per-project role: OWNER manages membership, LEAD manages the project but not\nmembership, MEMBER read/write, VIEWER read-only."
    },
    "ProjectMemberSummaryV1": {
      "properties": {
        "user_id": {
          "type": "string",
          "format": "uuid",
          "title": "User Id"
        },
        "email": {
          "type": "string",
          "title": "Email"
        },
        "role": {
          "$ref": "#/$defs/ProjectMemberRoleV1"
        }
      },
      "type": "object",
      "required": [
        "user_id",
        "email",
        "role"
      ],
      "title": "ProjectMemberSummaryV1",
      "description": "One member inside a project response, as returned by\n``?include=members``.\n\nNarrower than ``ProjectMemberV1`` from ``GET /projects/{id}/members``,\nwhich also carries first and last name: a roster embedded in every row of a\n100-project page pays for each field it carries, and display names are not\nwhat the roster is read for. Fetch the dedicated members endpoint for those.\n\n``user_id`` stays, though, because ``email`` cannot stand in for it. Two\naccounts may share an address (see ``User``), so an email-only entry is\nambiguous exactly when it matters, and it is ``user_id`` that the write\nside — ``PUT``/``DELETE /projects/{id}/members/{user_id}`` — is keyed by."
    },
    "ProjectRelatedCountsV1": {
      "properties": {
        "emails": {
          "type": "integer",
          "title": "Emails",
          "default": 0
        },
        "documents": {
          "type": "integer",
          "title": "Documents",
          "default": 0
        },
        "drawings": {
          "type": "integer",
          "title": "Drawings",
          "default": 0
        },
        "drawing_sets": {
          "type": "integer",
          "title": "Drawing Sets",
          "default": 0
        },
        "submittals": {
          "type": "integer",
          "title": "Submittals",
          "default": 0
        },
        "rfis": {
          "type": "integer",
          "title": "Rfis",
          "default": 0
        },
        "meetings": {
          "type": "integer",
          "title": "Meetings",
          "default": 0
        },
        "reports": {
          "type": "integer",
          "title": "Reports",
          "default": 0
        },
        "observations": {
          "type": "integer",
          "title": "Observations",
          "default": 0
        },
        "transmittals": {
          "type": "integer",
          "title": "Transmittals",
          "default": 0
        }
      },
      "type": "object",
      "title": "ProjectRelatedCountsV1",
      "description": "How many records of each type the project holds.\n\nEvery field defaults to 0, so a project with nothing filed under it\nreturns zeros rather than omitting keys.\n\nCounts cover the record types an all-time export of the same project\nships, with one exception: soft-deleted meetings are excluded, because\nthey are absent from every other read path. An export narrowed to a time\nrange ships a subset of what is counted here. Cogram's seeded example\nrecords are counted; they are real rows in the project.\n\nUsable as a change signal: poll, compare against the previous values, and\nre-export the projects that moved. Three classes of change are invisible\nto it, because cardinality does not move — an in-place edit (a renamed\ndocument, a corrected email subject); a new revision of an existing\ndrawing, or action items generated for an existing meeting, since only\nthe top-level record is counted; and an add plus a delete inside the same\npolling window, which nets to zero."
    }
  }
}