{"openapi":"3.1.0","info":{"title":"GPAgent API","version":"1.2.0","description":"Programmatic access to your fund data. Read/write portfolio companies, investor updates, metric snapshots, customer logos, data gaps, and fund-level stats. All endpoints require a Bearer API key.\n\n**Authorization model:** API keys are scoped to one or more organizations. Cross-org access (e.g. requesting a resource that exists but belongs to a different organization) returns **404 Not Found** with the same response body as a genuine missing resource. This existence-mask is intentional: an attacker holding a valid `gpa_*` key cannot enumerate another organization's resources by observing 403-vs-404 responses.\n\n**Breaking change (Phase 4.6, 2026-05-08):** endpoints that previously returned `403 FORBIDDEN` for cross-org access now return `404 NOT_FOUND`. `403` is reserved for the rare case where the response intentionally distinguishes \"authorized but blocked\" from \"not found\" — currently no portfolio-intel endpoint emits 403 for cross-org access.\n\n**Breaking change (Phase 4.7, 2026-05-08):** `POST /portfolio` now requires an explicit `vehicleSlug` in the request body. Previously it silently used the caller's first vehicle, which on 2026-05-08 caused six personal pre-fund investments to be created under the wrong vehicle. Discover valid slugs via `GET /vehicles`.","contact":{"url":"https://www.gpagent.ai"}},"servers":[{"url":"https://www.gpagent.ai/api/v1","description":"Production"},{"url":"http://localhost:3000/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"gpa_*","description":"API key prefixed with `gpa_`. Obtain from /app/settings/api."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["UNAUTHORIZED","FORBIDDEN","NOT_FOUND","BAD_REQUEST","CONFLICT","INTERNAL"]},"message":{"type":"string"}}}}},"InvestorUpdate":{"type":"object","properties":{"id":{"type":"string"},"companyId":{"type":"string"},"receivedAt":{"type":"string","format":"date-time"},"reportedPeriod":{"type":"string","nullable":true,"example":"2026-Q1"},"source":{"type":"string","enum":["email","manual","import"]},"sourceRef":{"type":"string","nullable":true},"fromAddress":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true},"rawBody":{"type":"string"},"rawHtml":{"type":"string","nullable":true},"attachmentsJson":{"nullable":true},"processedMetricsAt":{"type":"string","format":"date-time","nullable":true},"processedCustomersAt":{"type":"string","format":"date-time","nullable":true},"deletedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"MetricSnapshot":{"type":"object","properties":{"id":{"type":"string"},"companyId":{"type":"string"},"asOf":{"type":"string","format":"date-time"},"reportedPeriod":{"type":"string","nullable":true},"mrrUsd":{"type":"number","nullable":true},"arrUsd":{"type":"number","nullable":true},"cashOnHandUsd":{"type":"number","nullable":true},"burnMonthlyUsd":{"type":"number","nullable":true},"runwayMonths":{"type":"number","nullable":true},"postMoneyMarkedUsd":{"type":"number","nullable":true},"postMoneyImpliedUsd":{"type":"number","nullable":true},"lastRoundRevenueMultiple":{"type":"number","nullable":true},"headcount":{"type":"integer","nullable":true},"grossMarginPct":{"type":"number","nullable":true},"notes":{"type":"string","nullable":true},"sourceUpdateId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"Customer":{"type":"object","properties":{"id":{"type":"string"},"companyId":{"type":"string"},"customerName":{"type":"string"},"customerNameNormalized":{"type":"string"},"tier":{"type":"string","enum":["F100","F500","ENTERPRISE","MIDMARKET","SMB","GOV","EDU","OTHER"]},"status":{"type":"string","enum":["ACTIVE","PAUSED","CHURNED","PILOT","LOST_DEAL"]},"dealValueAnnualUsd":{"type":"number","nullable":true},"contractTerm":{"type":"string","nullable":true},"firstSignedAt":{"type":"string","format":"date-time","nullable":true},"churnedAt":{"type":"string","format":"date-time","nullable":true},"notes":{"type":"string","nullable":true},"deletedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"NewsItem":{"type":"object","properties":{"id":{"type":"string"},"companyId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"urlNormalized":{"type":"string"},"source":{"type":"string","nullable":true},"sourceTier":{"type":"integer","nullable":true},"publishedAt":{"type":"string","format":"date-time"},"summary":{"type":"string","nullable":true},"confidence":{"type":"number","nullable":true},"hidden":{"type":"boolean"},"materialNegative":{"type":"boolean"},"discoveredAt":{"type":"string","format":"date-time"},"discoveredBy":{"type":"string","nullable":true},"lastValidatedAt":{"type":"string","format":"date-time","nullable":true},"deletedAt":{"type":"string","format":"date-time","nullable":true}}},"DataGap":{"type":"object","properties":{"id":{"type":"string"},"companyId":{"type":"string"},"updateId":{"type":"string","nullable":true},"gapType":{"type":"string","enum":["ATTACHMENT_UNREAD","LINK_UNREAD","METRIC_MISSING","CUSTOMER_AMBIGUOUS","PERIOD_UNCLEAR"]},"status":{"type":"string","enum":["OPEN","RESOLVED","DISMISSED"]},"description":{"type":"string"},"suggestedField":{"type":"string","nullable":true},"hint":{"type":"string","nullable":true},"resolvedValue":{"nullable":true},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"resolvedBy":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DataGapEffect":{"type":"object","description":"Side-effect descriptor produced when resolving a data gap.","properties":{"kind":{"type":"string","enum":["metric-snapshot-created","customer-upserted","period-updated","content-stored","noop"]},"detail":{"type":"string"},"ref":{"type":"string","description":"Id of the affected row, when applicable."}}},"DataGapWithEffects":{"allOf":[{"$ref":"#/components/schemas/DataGap"},{"type":"object","properties":{"effects":{"type":"array","items":{"$ref":"#/components/schemas/DataGapEffect"}}}}]},"DataGapBulkResolveResponse":{"type":"object","properties":{"summary":{"type":"object","properties":{"resolved":{"type":"integer"},"alreadyResolved":{"type":"integer"},"notFound":{"type":"integer"},"invalid":{"type":"integer"},"total":{"type":"integer"}}},"items":{"type":"array","items":{"type":"object","properties":{"gapId":{"type":"string"},"status":{"type":"string","enum":["resolved","already-resolved","not-found","invalid"]},"message":{"type":"string"},"gap":{"$ref":"#/components/schemas/DataGap"},"effects":{"type":"array","items":{"$ref":"#/components/schemas/DataGapEffect"}}}}}}},"DataGapBulkDismissResponse":{"type":"object","properties":{"summary":{"type":"object","properties":{"dismissed":{"type":"integer"},"alreadyDismissed":{"type":"integer"},"notFound":{"type":"integer"},"total":{"type":"integer"}}},"items":{"type":"array","items":{"type":"object","properties":{"gapId":{"type":"string"},"status":{"type":"string","enum":["dismissed","already-dismissed","not-found"]},"gap":{"$ref":"#/components/schemas/DataGap"}}}}}},"Vehicle":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string","description":"Stable, human-readable id, unique within an organization (e.g. \"fund-i\", \"pre-fund-i\")."},"name":{"type":"string"},"type":{"type":"string","enum":["FUND","SPV","SYNDICATE","OTHER"],"description":"Vehicle classification"},"description":{"type":"string","nullable":true},"organizationId":{"type":"string"},"companyCount":{"type":"integer","description":"Number of portfolio companies in this vehicle (excludes soft-deleted)."},"createdAt":{"type":"string","format":"date-time"}}},"PortfolioCompany":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"website":{"type":"string","nullable":true},"sector":{"type":"string","nullable":true},"stage":{"type":"string","nullable":true},"checkSize":{"type":"number"},"postMoneyValuation":{"type":"number","nullable":true},"investmentDate":{"type":"string","format":"date-time"},"instrumentType":{"type":"string","enum":["SAFE","EQUITY","CONVERTIBLE"]},"status":{"type":"string","enum":["ACTIVE","MARKED_UP","WRITTEN_DOWN","EXITED"]},"notes":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"StatsSnapshot":{"type":"object","properties":{"id":{"type":"string"},"vehicleId":{"type":"string"},"computedAt":{"type":"string","format":"date-time"},"asOf":{"type":"string","format":"date-time"},"portfolioArrUsd":{"type":"number","nullable":true,"description":"Sum of ARR across all portcos with latest snapshots (USD)"},"portfolioArrYoyMultiple":{"type":"number","nullable":true,"description":"Median YoY ARR growth multiple across portcos"},"portfolioArrQoqMultiple":{"type":"number","nullable":true},"enterpriseLogoCoverage":{"type":"number","nullable":true,"description":"F100/F500 customers / total named customers"},"totalNamedCustomers":{"type":"integer","nullable":true},"totalF500Customers":{"type":"integer","nullable":true},"totalActiveCustomers":{"type":"integer","nullable":true},"computationJson":{"description":"Per-company breakdown used to compute the snapshot"},"createdAt":{"type":"string","format":"date-time"}}}}},"paths":{"/vehicles":{"get":{"tags":["Vehicles"],"summary":"List vehicles accessible to the caller","operationId":"listVehicles","description":"Returns every vehicle owned by an organization the caller has a membership in. Sorted by createdAt ascending. Use this to discover the `vehicleSlug` values you can pass to `POST /portfolio`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Vehicle"}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio":{"get":{"tags":["Portfolio"],"summary":"List portfolio companies in a vehicle","operationId":"listPortfolio","description":"Returns the vehicle, its fund model, and all non-soft-deleted portfolio companies. Multi-vehicle GPs **should** pass `?vehicleSlug=<slug>` (Phase 4.8a, 2026-05-09); without it, the endpoint falls back to the caller's default vehicle for backwards-compat. A slug that does not exist or belongs to another organization returns `404` (existence-mask).","parameters":[{"name":"vehicleSlug","in":"query","required":false,"schema":{"type":"string"},"description":"Vehicle slug (e.g. `fund-i`, `pre-fund-i`). When omitted, falls back to the caller's default vehicle."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Vehicle slug not found, or vehicle belongs to another organization (existence-mask).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Portfolio"],"summary":"Create a new portfolio company","operationId":"createPortfolioCompany","description":"Creates a portfolio company in the explicitly-selected vehicle. **As of Phase 4.7 (2026-05-08), `vehicleSlug` is required** — the endpoint no longer falls back to a default vehicle. Call `GET /vehicles` first to discover valid slugs. Strict create: returns 409 if the company slug already exists in the org.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","vehicleSlug","checkSize","investmentDate","instrumentType"],"properties":{"name":{"type":"string","description":"Display name"},"vehicleSlug":{"type":"string","description":"Slug of the vehicle this investment belongs to. Discoverable via `GET /vehicles`. Required to prevent silent vehicle co-mingling — see Phase 4.7 release notes."},"slug":{"type":"string","description":"Optional. Lowercase alphanumeric + hyphens. Derived from `name` if omitted."},"website":{"type":"string","format":"uri","nullable":true},"sector":{"type":"string","nullable":true},"stage":{"type":"string","nullable":true,"description":"e.g. pre-seed, seed, series-a"},"checkSize":{"type":"number","description":"USD, non-negative"},"postMoneyValuation":{"type":"number","nullable":true,"description":"USD, non-negative"},"investmentDate":{"type":"string","format":"date"},"instrumentType":{"type":"string","enum":["SAFE","EQUITY","CONVERTIBLE"]},"status":{"type":"string","enum":["ACTIVE","MARKED_UP","WRITTEN_DOWN","EXITED"],"default":"ACTIVE"},"notes":{"type":"string","nullable":true},"logoUrl":{"type":"string","format":"uri","nullable":true}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioCompany"}}}},"400":{"description":"Validation failed (missing/malformed fields, including missing vehicleSlug)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Vehicle slug not found, or vehicle belongs to another organization (existence-mask).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A portfolio company with the same slug already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/companies/{slug}/investor-updates":{"post":{"tags":["Investor Updates"],"summary":"Upload a raw investor update","operationId":"postInvestorUpdate","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["receivedAt","source","rawBody"],"properties":{"receivedAt":{"type":"string","format":"date-time"},"reportedPeriod":{"type":"string","example":"2026-Q1"},"source":{"type":"string","enum":["email","manual","import"]},"sourceRef":{"type":"string","description":"Gmail thread ID or other stable reference for idempotency"},"fromAddress":{"type":"string"},"subject":{"type":"string"},"rawBody":{"type":"string","minLength":1},"rawHtml":{"type":"string"},"attachmentsJson":{"description":"Attachment metadata (v1 stores metadata, not content)"}}}}}},"responses":{"200":{"description":"Already exists (idempotent sourceRef match)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestorUpdate"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestorUpdate"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Company not found, or company belongs to another organization (existence-mask — see API description)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Investor Updates"],"summary":"List investor updates for a company","operationId":"listInvestorUpdates","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"needsExtraction","in":"query","schema":{"type":"string","enum":["metrics","customers"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor (id of last item)"},{"name":"includeDeleted","in":"query","schema":{"type":"boolean"},"description":"When true, soft-deleted rows are included. Default false."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/InvestorUpdate"}},"nextCursor":{"type":"string","nullable":true}}}}}}}}},"/portfolio/companies/{slug}/investor-updates/{id}":{"get":{"tags":["Investor Updates"],"summary":"Get a single investor update","operationId":"getInvestorUpdate","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"includeDeleted","in":"query","schema":{"type":"boolean"},"description":"When true, soft-deleted rows are returned. Default false."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestorUpdate"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Investor Updates"],"summary":"Soft-delete an investor update","operationId":"deleteInvestorUpdate","description":"Sets deletedAt = now(). Idempotent. Does NOT cascade to derived metric snapshots (sourceUpdateId becomes a dangling pointer by design).","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Soft-deleted (or already deleted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestorUpdate"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/companies/{slug}/investor-updates/{id}/restore":{"post":{"tags":["Investor Updates"],"summary":"Restore a soft-deleted investor update","operationId":"restoreInvestorUpdate","description":"Clears deletedAt. Idempotent. Does not modify related resources.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Restored (or already non-deleted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestorUpdate"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/companies/{slug}/metric-snapshots":{"post":{"tags":["Metric Snapshots"],"summary":"Append a metric snapshot (append-only)","operationId":"postMetricSnapshot","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asOf"],"properties":{"asOf":{"type":"string","format":"date-time"},"reportedPeriod":{"type":"string"},"mrrUsd":{"type":"number"},"arrUsd":{"type":"number"},"cashOnHandUsd":{"type":"number"},"burnMonthlyUsd":{"type":"number"},"runwayMonths":{"type":"number"},"postMoneyMarkedUsd":{"type":"number"},"postMoneyImpliedUsd":{"type":"number"},"lastRoundRevenueMultiple":{"type":"number"},"headcount":{"type":"integer"},"grossMarginPct":{"type":"number","minimum":0,"maximum":100},"notes":{"type":"string"},"sourceUpdateId":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetricSnapshot"}}}}}},"get":{"tags":["Metric Snapshots"],"summary":"List metric snapshots for a company","operationId":"listMetricSnapshots","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MetricSnapshot"}}}}}}}}}},"/portfolio/companies/{slug}/customers":{"post":{"tags":["Customers"],"summary":"Upsert a customer logo","operationId":"upsertCustomer","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customerName","tier"],"properties":{"customerName":{"type":"string"},"tier":{"type":"string","enum":["F100","F500","ENTERPRISE","MIDMARKET","SMB","GOV","EDU","OTHER"]},"status":{"type":"string","enum":["ACTIVE","PAUSED","CHURNED","PILOT","LOST_DEAL"],"default":"ACTIVE"},"dealValueAnnualUsd":{"type":"number"},"contractTerm":{"type":"string"},"firstSignedAt":{"type":"string","format":"date-time"},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated (existing customer)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}}}},"get":{"tags":["Customers"],"summary":"List customers for a company","operationId":"listCustomers","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["ACTIVE","PAUSED","CHURNED","PILOT","LOST_DEAL"]}},{"name":"tier","in":"query","schema":{"type":"string","enum":["F100","F500","ENTERPRISE","MIDMARKET","SMB","GOV","EDU","OTHER"]}},{"name":"includeDeleted","in":"query","schema":{"type":"boolean"},"description":"When true, soft-deleted rows are included. Default false."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}}}}}}}}}},"/portfolio/companies/{slug}/metric-snapshots/{id}":{"delete":{"tags":["Metric Snapshots"],"summary":"Hard-delete a metric snapshot","operationId":"deleteMetricSnapshot","description":"Permanent delete. Snapshots are derived/append-only data; recomputable from upstream sources. Idempotent across the resource (404 once gone).","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/companies/{slug}/customers/{id}":{"delete":{"tags":["Customers"],"summary":"Soft-delete a customer","operationId":"deleteCustomer","description":"Sets deletedAt = now(). Idempotent. Distinct from CHURNED status (CHURNED = real-world churn; deletedAt = data hygiene).","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Soft-deleted (or already deleted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/companies/{slug}/customers/{id}/restore":{"post":{"tags":["Customers"],"summary":"Restore a soft-deleted customer","operationId":"restoreCustomer","description":"Clears deletedAt. Idempotent.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Restored (or already non-deleted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/companies/{slug}/customers/{id}/churn":{"post":{"tags":["Customers"],"summary":"Mark a customer as churned","operationId":"churnCustomer","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["churnedAt"],"properties":{"churnedAt":{"type":"string","format":"date-time"},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"OK (churned or already churned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}}}}},"/portfolio/companies/{slug}/data-gaps":{"post":{"tags":["Data Gaps"],"summary":"Create a data gap report","operationId":"createDataGap","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["gapType","description"],"properties":{"gapType":{"type":"string","enum":["ATTACHMENT_UNREAD","LINK_UNREAD","METRIC_MISSING","CUSTOMER_AMBIGUOUS","PERIOD_UNCLEAR"]},"description":{"type":"string"},"updateId":{"type":"string"},"suggestedField":{"type":"string"},"hint":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataGap"}}}}}}},"/portfolio/data-gaps/{id}":{"delete":{"tags":["Data Gaps"],"summary":"Hard-delete a data gap","operationId":"deleteDataGap","description":"Permanent delete. Idempotent across the resource (404 once gone). Cross-org access returns 404 (existence-mask).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found, or gap belongs to another organization (existence-mask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/data-gaps":{"get":{"tags":["Data Gaps"],"summary":"List data gaps across the caller's orgs","operationId":"listDataGaps","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["OPEN","RESOLVED","DISMISSED"],"default":"OPEN"}},{"name":"companyId","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/DataGap"}}}}}}}}}},"/portfolio/data-gaps/{id}/resolve":{"post":{"tags":["Data Gaps"],"summary":"Resolve a data gap (with side-effects)","description":"Resolves a data gap with the supplied value AND propagates the value to its destination in the data model:\n\n- METRIC_MISSING: appends a PortfolioCompanyMetricSnapshot row.\n- CUSTOMER_AMBIGUOUS: upserts a PortfolioCompanyCustomer row.\n- PERIOD_UNCLEAR: updates the source InvestorUpdate.reportedPeriod.\n- ATTACHMENT_UNREAD/LINK_UNREAD: content saved on gap; next weekly extraction reads it.\n\nIdempotent: re-resolving a RESOLVED gap returns the current state without re-running side-effects.","operationId":"resolveDataGap","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resolvedValue"],"properties":{"resolvedValue":{"description":"The resolved value (number for METRIC_MISSING, object for CUSTOMER_AMBIGUOUS, string for PERIOD_UNCLEAR, etc.)"},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataGapWithEffects"}}}}}}},"/portfolio/data-gaps/{id}/dismiss":{"post":{"tags":["Data Gaps"],"summary":"Dismiss a data gap","description":"Marks a gap as DISMISSED (\"not going to fill this in\"). Distinct from RESOLVED (which provides a value) and DELETE (which removes the row). Idempotent.","operationId":"dismissDataGap","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","description":"Optional reason for dismissal; appended to the gap hint."}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataGap"}}}}}}},"/portfolio/data-gaps/bulk-resolve":{"post":{"tags":["Data Gaps"],"summary":"Resolve up to 100 data gaps at once","description":"Per-item partial success: a bad item does not 4xx the batch. Each item gets its own status in the response (resolved, already-resolved, not-found, invalid). Hard cap of 100 per batch.","operationId":"bulkResolveDataGaps","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resolutions"],"properties":{"resolutions":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["gapId","resolvedValue"],"properties":{"gapId":{"type":"string"},"resolvedValue":{},"notes":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"OK (per-item statuses in the response body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataGapBulkResolveResponse"}}}},"400":{"description":"Bad request (e.g. > 100 items)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/data-gaps/bulk-dismiss":{"post":{"tags":["Data Gaps"],"summary":"Dismiss up to 100 data gaps at once","description":"Per-item partial success. Hard cap of 100 per batch.","operationId":"bulkDismissDataGaps","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["gapIds"],"properties":{"gapIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"}},"notes":{"type":"string","description":"Optional reason; appended to each dismissed gap hint."}}}}}},"responses":{"200":{"description":"OK (per-item statuses in the response body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataGapBulkDismissResponse"}}}},"400":{"description":"Bad request (e.g. > 100 items)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/news/{id}":{"get":{"tags":["News"],"summary":"Get a single news item","operationId":"getNewsItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"includeDeleted","in":"query","schema":{"type":"boolean"},"description":"When true, soft-deleted rows are returned. Default false."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsItem"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["News"],"summary":"Soft-delete a news item","operationId":"deleteNewsItem","description":"Sets deletedAt = now(). Distinct from `hidden`: `hidden` de-prioritizes in the feed; `deletedAt` removes from the API surface entirely. Idempotent.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Soft-deleted (or already deleted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsItem"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/stats/{id}":{"delete":{"tags":["Stats"],"summary":"Hard-delete a fund stats snapshot","operationId":"deleteStatsSnapshot","description":"Permanent delete. Stats are derived from current portfolio state and recomputable at any time. Idempotent across the resource (404 once gone). Cross-org access returns 404 (existence-mask).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found, or snapshot belongs to another organization (existence-mask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/news":{"post":{"tags":["News"],"summary":"Bulk-ingest portfolio company news","operationId":"ingestNews","description":"Auto-publishes based on source tier and confidence. Idempotent per URL. Org-scoped per-item: items targeting a company outside the caller's organizations are rejected with status `rejected_wrong_org` and the rest of the batch continues.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["companySlug","url","title","publishedAt"],"properties":{"companySlug":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"publishedAt":{"type":"string","format":"date-time"},"source":{"type":"string"},"summary":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1},"discoveredBy":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","properties":{"published":{"type":"integer"},"updated":{"type":"integer"},"rejected":{"type":"integer"},"total":{"type":"integer"}}},"items":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"status":{"type":"string","enum":["published","updated","rejected_low_confidence","rejected_no_mention","rejected_company_unknown","rejected_wrong_org","rejected_invalid"]},"reason":{"type":"string"}}}}}}}}}}}},"/portfolio/news/maintenance":{"post":{"tags":["News"],"summary":"Self-healing maintenance pass for the news feed","operationId":"newsMaintenance","description":"Org-scoped: only operates on news items whose company belongs to one of the caller's organizations (SQL-level filter). Re-validates stale URLs, archives items older than 365 days, returns recent material-negative items for alerting. Idempotent. Designed for daily cron. Returns zero stats if the caller has no accessible companies.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"stats":{"type":"object","properties":{"revalidated":{"type":"integer"},"autoHidden":{"type":"integer"},"archivedByAge":{"type":"integer"},"newNegatives":{"type":"integer"}}},"materialNegatives":{"type":"array"}}}}}}}}},"/portfolio/stats/recompute":{"post":{"tags":["Stats"],"summary":"Recompute and persist a fund stats snapshot","description":"Accepts either `vehicleSlug` (preferred, matches the rest of v1) or `vehicleId` (deprecated cuid form, kept for backwards compat with pre-Phase-7b callers). At least one is required; if both are provided, `vehicleSlug` wins.","operationId":"recomputeStats","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Provide vehicleSlug (preferred) or vehicleId.","properties":{"vehicleSlug":{"type":"string","description":"Vehicle slug, e.g. `fund-i`. Discoverable via `GET /vehicles`. Preferred form."},"vehicleId":{"type":"string","description":"Vehicle cuid. **Deprecated** — kept for backwards compat with pre-Phase-7b callers (notably the Phase 6 fund-intelligence cron). Prefer `vehicleSlug`."}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsSnapshot"}}}},"400":{"description":"Neither vehicleSlug nor vehicleId provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Vehicle not found, or belongs to another organization (existence-mask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/portfolio/stats/latest":{"get":{"tags":["Stats"],"summary":"Get the most recent stats snapshot for a vehicle","description":"Accepts either `?vehicleSlug=...` (preferred) or `?vehicleId=...` (deprecated cuid form). At least one is required; if both are passed, slug wins.","operationId":"latestStats","parameters":[{"name":"vehicleSlug","in":"query","required":false,"schema":{"type":"string"},"description":"Vehicle slug, e.g. `fund-i`. Preferred form."},{"name":"vehicleId","in":"query","required":false,"schema":{"type":"string"},"description":"Vehicle cuid. **Deprecated** — prefer `vehicleSlug`."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsSnapshot"}}}},"400":{"description":"Neither vehicleSlug nor vehicleId provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No snapshot yet, vehicle not found, or vehicle belongs to another org (existence-mask)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}