{
  "openapi": "3.1.0",
  "info": {
    "title": "CE Canvas Partner API",
    "version": "1.0.0",
    "description": "Lets a connected partner system add documents and boundaries to CE Canvas projects. Every request runs as the installed credential; row-level security decides what it may reach. See docs/integrations/PARTNER_API_V1.md for the narrative spec.",
    "contact": { "email": "support@cecanvas.com" }
  },
  "servers": [
    { "url": "https://app.cecanvas.com/api/v1", "description": "Production (Australia)" },
    { "url": "https://staging.cecanvas.com/api/v1", "description": "Staging — the partner test tenant" }
  ],
  "security": [{ "credential": [] }],
  "tags": [
    { "name": "projects", "description": "Which projects a credential may write to." },
    { "name": "resources", "description": "Documents: declare, upload, complete, watch." },
    { "name": "spatial", "description": "Boundaries and the measures that describe them." }
  ],
  "paths": {
    "/projects": {
      "get": {
        "tags": ["projects"],
        "operationId": "listProjects",
        "summary": "List the projects this credential can reach",
        "parameters": [
          { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" }, "description": "next_cursor from the previous page." },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "responses": {
          "200": { "description": "A page of projects.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectListResponse" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/projects/{project_id}": {
      "get": {
        "tags": ["projects"],
        "operationId": "getProject",
        "summary": "One project, with whether it already has a project area",
        "parameters": [
          { "$ref": "#/components/parameters/ProjectId" },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "responses": {
          "200": { "description": "The project.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectDetail" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/projects/{project_id}/resources": {
      "post": {
        "tags": ["resources"],
        "operationId": "createResource",
        "summary": "Declare a document and receive a signed upload target",
        "description": "Idempotent on Idempotency-Key. Replaying the same key and body returns the same resource with a freshly minted upload target while the upload is still expected. The same key with a different body is a 409 idempotency_conflict; a new key with an external_id already used is a 409 duplicate_external_id.",
        "parameters": [
          { "$ref": "#/components/parameters/ProjectId" },
          { "$ref": "#/components/parameters/IdempotencyKey" },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateResourceRequest" } } } },
        "responses": {
          "201": { "description": "Declared. PUT the bytes to upload.url, then call upload-complete.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateResourceResponse" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/projects/{project_id}/spatial-features": {
      "post": {
        "tags": ["spatial"],
        "operationId": "createSpatialFeature",
        "summary": "Send a boundary and its measures",
        "description": "The boundary lands as a reference layer: visible on the project's map with your attribution, but not part of the project area until a practitioner adopts it. The same source.external_id replaces the layer; measures for that boundary are replaced as a set. Requires the organisation to have Project Area enabled (403 feature_disabled otherwise).",
        "parameters": [
          { "$ref": "#/components/parameters/ProjectId" },
          { "$ref": "#/components/parameters/IdempotencyKey" },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSpatialFeatureRequest" } } } },
        "responses": {
          "201": { "description": "Stored as a reference layer.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpatialFeature" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/resources/{resource_id}": {
      "get": {
        "tags": ["resources"],
        "operationId": "getResource",
        "summary": "Upload, extraction and indexing status",
        "parameters": [
          { "$ref": "#/components/parameters/ResourceId" },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "responses": {
          "200": { "description": "Current status.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceStatus" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/resources/{resource_id}/upload-complete": {
      "post": {
        "tags": ["resources"],
        "operationId": "completeResourceUpload",
        "summary": "Tell CE Canvas the bytes are uploaded",
        "description": "CE Canvas reads the stored object and checks its byte count, content type and sha256 against the declaration. On success the document is queued for extraction and indexing. A mismatch deletes the object, marks the resource failed and returns 422 upload_verification_failed. Calling again on a completed resource is a no-op that returns the current status.",
        "parameters": [
          { "$ref": "#/components/parameters/ResourceId" },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "responses": {
          "200": { "description": "Verified and queued, or the current status.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceStatus" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/spatial-features/{feature_id}": {
      "get": {
        "tags": ["spatial"],
        "operationId": "getSpatialFeature",
        "summary": "Whether a boundary is still a reference layer or has been adopted",
        "parameters": [
          { "$ref": "#/components/parameters/FeatureId" },
          { "$ref": "#/components/parameters/RequestId" }
        ],
        "responses": {
          "200": { "description": "The boundary.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SpatialFeature" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "credential": {
        "type": "http",
        "scheme": "bearer",
        "description": "The installation credential issued in CE Canvas (Settings → Integrations). Format cec_live_… . Send as Authorization: Bearer <credential>."
      }
    },
    "parameters": {
      "ProjectId": { "name": "project_id", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^prj_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" } },
      "ResourceId": { "name": "resource_id", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^res_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" } },
      "FeatureId": { "name": "feature_id", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^sf_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" } },
      "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 8, "maxLength": 128 }, "description": "Stable per logical request. Same key + same body replays; same key + different body is 409." },
      "RequestId": { "name": "X-Request-Id", "in": "header", "required": false, "schema": { "type": "string", "minLength": 8, "maxLength": 128 }, "description": "Optional correlation id. Echoed on every response; generated when absent." }
    },
    "responses": {
      "Error": {
        "description": "The shared error envelope.",
        "headers": { "X-Request-Id": { "schema": { "type": "string" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "additionalProperties": true,
        "required": ["type", "title", "status", "request_id"],
        "properties": {
          "type": { "type": "string", "enum": ["unauthorized", "forbidden", "insufficient_scope", "not_found", "method_not_allowed", "validation_failed", "payload_too_large", "limit_exceeded", "feature_disabled", "idempotency_key_required", "idempotency_conflict", "idempotency_in_progress", "duplicate_external_id", "invalid_state", "upload_verification_failed", "internal_error"] },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "detail": { "type": "string" },
          "request_id": { "type": "string" }
        }
      },
      "ProjectCapabilities": {
        "type": "object",
        "required": ["resources_write", "spatial_write"],
        "properties": {
          "resources_write": { "type": "boolean", "description": "The credential holds resources:write." },
          "spatial_write": { "type": "boolean", "description": "The credential holds spatial:write and the organisation has Project Area enabled." }
        }
      },
      "Organisation": {
        "type": "object",
        "required": ["id", "name"],
        "properties": {
          "id": { "type": "string", "pattern": "^org_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "name": { "type": "string" }
        }
      },
      "ProjectSummary": {
        "type": "object",
        "required": ["id", "name", "organisation", "capabilities", "created_at"],
        "properties": {
          "id": { "type": "string", "pattern": "^prj_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "name": { "type": "string" },
          "organisation": { "$ref": "#/components/schemas/Organisation" },
          "capabilities": { "$ref": "#/components/schemas/ProjectCapabilities" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "ProjectDetail": {
        "type": "object",
        "required": ["id", "name", "organisation", "capabilities", "created_at", "has_project_area"],
        "properties": {
          "id": { "type": "string", "pattern": "^prj_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "name": { "type": "string" },
          "organisation": { "$ref": "#/components/schemas/Organisation" },
          "capabilities": { "$ref": "#/components/schemas/ProjectCapabilities" },
          "created_at": { "type": "string", "format": "date-time" },
          "has_project_area": { "type": "boolean", "description": "True when the project already has a resolved project area." }
        }
      },
      "ProjectListResponse": {
        "type": "object",
        "required": ["data", "next_cursor"],
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectSummary" } },
          "next_cursor": { "type": "string", "nullable": true }
        }
      },
      "ResourceSource": {
        "type": "object",
        "required": ["external_id"],
        "properties": {
          "external_id": { "type": "string", "minLength": 1, "maxLength": 256, "description": "Your id for this report. Unique per credential." },
          "generated_at": { "type": "string", "format": "date-time" },
          "url": { "type": "string", "format": "uri", "maxLength": 2000, "description": "A link back to the report in your product." }
        }
      },
      "CreateResourceRequest": {
        "type": "object",
        "required": ["title", "filename", "content_type", "content_length", "sha256", "source"],
        "properties": {
          "title": { "type": "string", "minLength": 1, "maxLength": 200 },
          "filename": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^[^/\\\\ ]+$" },
          "content_type": { "type": "string", "enum": ["application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "text/plain"] },
          "content_length": { "type": "integer", "minimum": 1, "maximum": 52428800 },
          "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "Lower-case hex sha256 of the exact bytes you will upload." },
          "category": { "type": "string", "enum": ["report", "policy", "strategy", "notes", "other"], "default": "report" },
          "include_in_ai": { "type": "boolean", "default": true, "description": "Extract and index the document for EVA and project search." },
          "source": { "$ref": "#/components/schemas/ResourceSource" }
        }
      },
      "StageStatus": { "type": "string", "enum": ["pending", "processing", "completed", "failed", "skipped"] },
      "ResourceStages": {
        "type": "object",
        "required": ["upload", "text_extraction", "indexing"],
        "properties": {
          "upload": { "$ref": "#/components/schemas/StageStatus" },
          "text_extraction": { "$ref": "#/components/schemas/StageStatus" },
          "indexing": { "$ref": "#/components/schemas/StageStatus" }
        }
      },
      "ResourceStatus": {
        "type": "object",
        "required": ["id", "project_id", "title", "filename", "content_type", "content_length", "status", "stages", "available_in_project", "available_in_search", "available_to_eva", "error", "source", "created_at", "updated_at"],
        "properties": {
          "id": { "type": "string", "pattern": "^res_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "project_id": { "type": "string", "pattern": "^prj_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "title": { "type": "string" },
          "filename": { "type": "string" },
          "content_type": { "type": "string" },
          "content_length": { "type": "integer" },
          "status": { "type": "string", "enum": ["awaiting_upload", "verifying", "processing", "completed", "failed"] },
          "stages": { "$ref": "#/components/schemas/ResourceStages" },
          "available_in_project": { "type": "boolean", "description": "Stored and listed in the project's Resources." },
          "available_in_search": { "type": "boolean", "description": "Indexed and searchable." },
          "available_to_eva": { "type": "boolean", "description": "Indexed AND marked for AI use, so EVA can cite it." },
          "error": { "type": "string", "nullable": true, "description": "A short class such as checksum_mismatch; never an internal message." },
          "source": { "$ref": "#/components/schemas/ResourceSource" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "UploadTarget": {
        "type": "object",
        "required": ["method", "url", "headers", "expires_at"],
        "properties": {
          "method": { "type": "string", "enum": ["PUT"] },
          "url": { "type": "string", "format": "uri" },
          "headers": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Send these verbatim with the PUT." },
          "expires_at": { "type": "string", "format": "date-time" }
        }
      },
      "CreateResourceResponse": {
        "type": "object",
        "required": ["id", "project_id", "title", "filename", "content_type", "content_length", "status", "stages", "available_in_project", "available_in_search", "available_to_eva", "error", "source", "created_at", "updated_at", "upload"],
        "properties": {
          "id": { "type": "string", "pattern": "^res_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "project_id": { "type": "string", "pattern": "^prj_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "title": { "type": "string" },
          "filename": { "type": "string" },
          "content_type": { "type": "string" },
          "content_length": { "type": "integer" },
          "status": { "type": "string", "enum": ["awaiting_upload", "verifying", "processing", "completed", "failed"] },
          "stages": { "$ref": "#/components/schemas/ResourceStages" },
          "available_in_project": { "type": "boolean" },
          "available_in_search": { "type": "boolean" },
          "available_to_eva": { "type": "boolean" },
          "error": { "type": "string", "nullable": true },
          "source": { "$ref": "#/components/schemas/ResourceSource" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "upload": { "allOf": [{ "$ref": "#/components/schemas/UploadTarget" }], "nullable": true, "description": "Present while the upload is still expected; null once completed." }
        }
      },
      "Geometry": {
        "description": "GeoJSON Polygon or MultiPolygon in WGS84 (EPSG:4326), at most 20000 vertices.",
        "oneOf": [
          { "type": "object", "required": ["type", "coordinates"], "properties": { "type": { "type": "string", "enum": ["Polygon"] }, "coordinates": { "type": "array", "items": { "type": "array", "items": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 3 } } } } },
          { "type": "object", "required": ["type", "coordinates"], "properties": { "type": { "type": "string", "enum": ["MultiPolygon"] }, "coordinates": { "type": "array", "items": { "type": "array", "items": { "type": "array", "items": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 3 } } } } } }
        ]
      },
      "AreaRole": { "type": "string", "enum": ["project_area", "engagement_area", "other"] },
      "SpatialSource": {
        "type": "object",
        "required": ["external_id"],
        "properties": {
          "external_id": { "type": "string", "minLength": 1, "maxLength": 256, "description": "Your id for this boundary. Sending it again replaces the layer." },
          "url": { "type": "string", "format": "uri", "maxLength": 2000 },
          "generated_at": { "type": "string", "format": "date-time" }
        }
      },
      "PartnerMeasure": {
        "type": "object",
        "required": ["code", "label", "value", "unit", "reference_year", "source"],
        "properties": {
          "code": { "type": "string", "minLength": 1, "maxLength": 80, "description": "Stable within your product, e.g. age_65_plus." },
          "label": { "type": "string", "minLength": 1, "maxLength": 160, "description": "Shown verbatim beside the value." },
          "value": { "type": "number" },
          "denominator": { "type": "number", "nullable": true, "minimum": 0, "description": "What value is a share of. Omit for a standalone count." },
          "unit": { "type": "string", "minLength": 1, "maxLength": 40, "description": "persons, dwellings, households, businesses, …" },
          "reference_year": { "type": "integer", "minimum": 1900, "maximum": 2100 },
          "source": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The publisher of the underlying figure, in full." },
          "provider_version": { "type": "string", "maxLength": 80 },
          "attribution_method": { "type": "string", "maxLength": 200, "description": "How the figure was attributed to this boundary, e.g. area_weighted." },
          "caveats": { "type": "array", "items": { "type": "string", "maxLength": 500 }, "maxItems": 10, "default": [] }
        }
      },
      "CreateSpatialFeatureRequest": {
        "type": "object",
        "required": ["geometry", "name", "source"],
        "properties": {
          "geometry": { "$ref": "#/components/schemas/Geometry" },
          "proposed_role": { "allOf": [{ "$ref": "#/components/schemas/AreaRole" }], "default": "project_area", "description": "What you think this boundary is. Offered as the default when a practitioner adopts it." },
          "name": { "type": "string", "minLength": 1, "maxLength": 120 },
          "description": { "type": "string", "maxLength": 4000 },
          "source": { "$ref": "#/components/schemas/SpatialSource" },
          "measures": { "type": "array", "items": { "$ref": "#/components/schemas/PartnerMeasure" }, "maxItems": 200, "default": [] }
        }
      },
      "SpatialFeature": {
        "type": "object",
        "required": ["id", "project_id", "name", "proposed_role", "state", "adopted_role", "area_m2", "bbox", "source", "measure_count", "created_at", "updated_at"],
        "properties": {
          "id": { "type": "string", "pattern": "^sf_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "project_id": { "type": "string", "pattern": "^prj_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
          "name": { "type": "string", "nullable": true },
          "proposed_role": { "$ref": "#/components/schemas/AreaRole" },
          "state": { "type": "string", "enum": ["reference_layer", "adopted"], "description": "reference_layer until a practitioner adopts it into the project area." },
          "adopted_role": { "allOf": [{ "$ref": "#/components/schemas/AreaRole" }], "nullable": true },
          "area_m2": { "type": "number", "nullable": true },
          "bbox": { "type": "array", "nullable": true, "items": { "type": "number" }, "minItems": 4, "maxItems": 4 },
          "source": { "$ref": "#/components/schemas/SpatialSource" },
          "measure_count": { "type": "integer", "minimum": 0 },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  }
}
