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. **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. **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. **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`.
Base URL: https://www.gpagent.ai/api/v1Auth: Bearer gpa_...Authentication: All endpoints require Authorization: Bearer gpa_.... Manage keys at /app/settings/api.
/vehiclesList vehicles accessible to the caller
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`.
Success: 200 - OK
Errors: 401 - Unauthorized
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/vehicles"
/portfolioList portfolio companies in a vehicle
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).
Success: 200 - OK
Errors: 401 - Unauthorized, 404 - Vehicle slug not found, or vehicle belongs to another organization (existence-mask).
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio"
/portfolioCreate a new portfolio company
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.
Request body
| Field | Type | Required |
|---|---|---|
name | string | Yes |
vehicleSlug | string | Yes |
slug | string | No |
website | string | No |
sector | string | No |
stage | string | No |
checkSize | number | Yes |
postMoneyValuation | number | No |
investmentDate | string | Yes |
instrumentType | SAFE | EQUITY | CONVERTIBLE | Yes |
Success: 201 - Created
Errors: 400 - Validation failed (missing/malformed fields, including missing vehicleSlug), 401 - Unauthorized, 404 - Vehicle slug not found, or vehicle belongs to another organization (existence-mask)., 409 - A portfolio company with the same slug already exists.
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"<name>","vehicleSlug":"<vehicleSlug>","checkSize":0,"investmentDate":"<investmentDate>"}' \
"https://www.gpagent.ai/api/v1/portfolio"/portfolio/companies/{slug}/investor-updatesUpload a raw investor update
Request body
| Field | Type | Required |
|---|---|---|
receivedAt | string | Yes |
reportedPeriod | string | No |
source | email | manual | import | Yes |
sourceRef | string | No |
fromAddress | string | No |
subject | string | No |
rawBody | string | Yes |
rawHtml | string | No |
attachmentsJson | any | No |
Success: 200 - Already exists (idempotent sourceRef match), 201 - Created
Errors: 400 - Bad request, 401 - Unauthorized, 404 - Company not found, or company belongs to another organization (existence-mask — see API description)
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"receivedAt":"2026-01-01T00:00:00Z","source":"email","rawBody":"<rawBody>"}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/investor-updates"/portfolio/companies/{slug}/investor-updatesList investor updates for a company
Success: 200 - OK
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/investor-updates"
/portfolio/companies/{slug}/investor-updates/{id}Get a single investor update
Success: 200 - OK
Errors: 404 - Not found
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/investor-updates/id_value"
/portfolio/companies/{slug}/investor-updates/{id}Soft-delete an investor update
Sets deletedAt = now(). Idempotent. Does NOT cascade to derived metric snapshots (sourceUpdateId becomes a dangling pointer by design).
Success: 200 - Soft-deleted (or already deleted)
Errors: 404 - Not found
Example
curl -X DELETE \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/investor-updates/id_value"
/portfolio/companies/{slug}/investor-updates/{id}/restoreRestore a soft-deleted investor update
Clears deletedAt. Idempotent. Does not modify related resources.
Success: 200 - Restored (or already non-deleted)
Errors: 404 - Not found
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/investor-updates/id_value/restore"/portfolio/companies/{slug}/metric-snapshotsAppend a metric snapshot (append-only)
Request body
| Field | Type | Required |
|---|---|---|
asOf | string | Yes |
reportedPeriod | string | No |
mrrUsd | number | No |
arrUsd | number | No |
cashOnHandUsd | number | No |
burnMonthlyUsd | number | No |
runwayMonths | number | No |
postMoneyMarkedUsd | number | No |
postMoneyImpliedUsd | number | No |
lastRoundRevenueMultiple | number | No |
Success: 201 - Created
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"asOf":"2026-01-01T00:00:00Z"}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/metric-snapshots"/portfolio/companies/{slug}/metric-snapshotsList metric snapshots for a company
Success: 200 - OK
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/metric-snapshots"
/portfolio/companies/{slug}/metric-snapshots/{id}Hard-delete a metric snapshot
Permanent delete. Snapshots are derived/append-only data; recomputable from upstream sources. Idempotent across the resource (404 once gone).
Success: 204 - Deleted
Errors: 404 - Not found
Example
curl -X DELETE \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/metric-snapshots/id_value"
/portfolio/companies/{slug}/customersUpsert a customer logo
Request body
| Field | Type | Required |
|---|---|---|
customerName | string | Yes |
tier | F100 | F500 | ENTERPRISE | MIDMARKET | SMB | GOV | EDU | OTHER | Yes |
status | ACTIVE | PAUSED | CHURNED | PILOT | LOST_DEAL | No |
dealValueAnnualUsd | number | No |
contractTerm | string | No |
firstSignedAt | string | No |
notes | string | No |
Success: 200 - Updated (existing customer), 201 - Created
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customerName":"<customerName>","tier":"F100"}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/customers"/portfolio/companies/{slug}/customersList customers for a company
Success: 200 - OK
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/customers"
/portfolio/companies/{slug}/customers/{id}Soft-delete a customer
Sets deletedAt = now(). Idempotent. Distinct from CHURNED status (CHURNED = real-world churn; deletedAt = data hygiene).
Success: 200 - Soft-deleted (or already deleted)
Errors: 404 - Not found
Example
curl -X DELETE \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/customers/id_value"
/portfolio/companies/{slug}/customers/{id}/restoreRestore a soft-deleted customer
Clears deletedAt. Idempotent.
Success: 200 - Restored (or already non-deleted)
Errors: 404 - Not found
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/customers/id_value/restore"/portfolio/companies/{slug}/customers/{id}/churnMark a customer as churned
Request body
| Field | Type | Required |
|---|---|---|
churnedAt | string | Yes |
notes | string | No |
Success: 200 - OK (churned or already churned)
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"churnedAt":"2026-01-01T00:00:00Z"}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/customers/id_value/churn"/portfolio/companies/{slug}/data-gapsCreate a data gap report
Request body
| Field | Type | Required |
|---|---|---|
gapType | ATTACHMENT_UNREAD | LINK_UNREAD | METRIC_MISSING | CUSTOMER_AMBIGUOUS | PERIOD_UNCLEAR | Yes |
description | string | Yes |
updateId | string | No |
suggestedField | string | No |
hint | string | No |
Success: 201 - Created
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"gapType":"ATTACHMENT_UNREAD","description":"<description>"}' \
"https://www.gpagent.ai/api/v1/portfolio/companies/slug_value/data-gaps"/portfolio/data-gaps/{id}Hard-delete a data gap
Permanent delete. Idempotent across the resource (404 once gone). Cross-org access returns 404 (existence-mask).
Success: 204 - Deleted
Errors: 404 - Not found, or gap belongs to another organization (existence-mask)
Example
curl -X DELETE \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/data-gaps/id_value"
/portfolio/data-gapsList data gaps across the caller's orgs
Success: 200 - OK
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/data-gaps"
/portfolio/data-gaps/{id}/resolveResolve a data gap (with side-effects)
Resolves a data gap with the supplied value AND propagates the value to its destination in the data model: - METRIC_MISSING: appends a PortfolioCompanyMetricSnapshot row. - CUSTOMER_AMBIGUOUS: upserts a PortfolioCompanyCustomer row. - PERIOD_UNCLEAR: updates the source InvestorUpdate.reportedPeriod. - ATTACHMENT_UNREAD/LINK_UNREAD: content saved on gap; next weekly extraction reads it. Idempotent: re-resolving a RESOLVED gap returns the current state without re-running side-effects.
Request body
| Field | Type | Required |
|---|---|---|
resolvedValue | any | Yes |
notes | string | No |
Success: 200 - OK
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"resolvedValue":"<resolvedValue>"}' \
"https://www.gpagent.ai/api/v1/portfolio/data-gaps/id_value/resolve"/portfolio/data-gaps/{id}/dismissDismiss a data gap
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.
Request body
| Field | Type | Required |
|---|---|---|
notes | string | No |
Success: 200 - OK
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"https://www.gpagent.ai/api/v1/portfolio/data-gaps/id_value/dismiss"/portfolio/data-gaps/bulk-resolveResolve up to 100 data gaps at once
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.
Request body
| Field | Type | Required |
|---|---|---|
resolutions | array | Yes |
Success: 200 - OK (per-item statuses in the response body)
Errors: 400 - Bad request (e.g. > 100 items)
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"resolutions":"<resolutions>"}' \
"https://www.gpagent.ai/api/v1/portfolio/data-gaps/bulk-resolve"/portfolio/data-gaps/bulk-dismissDismiss up to 100 data gaps at once
Per-item partial success. Hard cap of 100 per batch.
Request body
| Field | Type | Required |
|---|---|---|
gapIds | array | Yes |
notes | string | No |
Success: 200 - OK (per-item statuses in the response body)
Errors: 400 - Bad request (e.g. > 100 items)
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"gapIds":"<gapIds>"}' \
"https://www.gpagent.ai/api/v1/portfolio/data-gaps/bulk-dismiss"/portfolio/news/{id}Get a single news item
Success: 200 - OK
Errors: 404 - Not found
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/news/id_value"
/portfolio/news/{id}Soft-delete a news item
Sets deletedAt = now(). Distinct from `hidden`: `hidden` de-prioritizes in the feed; `deletedAt` removes from the API surface entirely. Idempotent.
Success: 200 - Soft-deleted (or already deleted)
Errors: 404 - Not found
Example
curl -X DELETE \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/news/id_value"
/portfolio/newsBulk-ingest portfolio company news
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.
Request body
| Field | Type | Required |
|---|---|---|
items | array | Yes |
Success: 200 - OK
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"items":"<items>"}' \
"https://www.gpagent.ai/api/v1/portfolio/news"/portfolio/news/maintenanceSelf-healing maintenance pass for the news feed
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.
Success: 200 - OK
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"https://www.gpagent.ai/api/v1/portfolio/news/maintenance"/portfolio/stats/{id}Hard-delete a fund stats snapshot
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).
Success: 204 - Deleted
Errors: 404 - Not found, or snapshot belongs to another organization (existence-mask)
Example
curl -X DELETE \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/stats/id_value"
/portfolio/stats/recomputeRecompute and persist a fund stats snapshot
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.
Request body
| Field | Type | Required |
|---|---|---|
vehicleSlug | string | No |
vehicleId | string | No |
Success: 201 - Created
Errors: 400 - Neither vehicleSlug nor vehicleId provided, 404 - Vehicle not found, or belongs to another organization (existence-mask)
Example
curl -X POST \
-H "Authorization: Bearer $GPAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"https://www.gpagent.ai/api/v1/portfolio/stats/recompute"/portfolio/stats/latestGet the most recent stats snapshot for a vehicle
Accepts either `?vehicleSlug=...` (preferred) or `?vehicleId=...` (deprecated cuid form). At least one is required; if both are passed, slug wins.
Success: 200 - OK
Errors: 400 - Neither vehicleSlug nor vehicleId provided, 404 - No snapshot yet, vehicle not found, or vehicle belongs to another org (existence-mask)
Example
curl -X GET \ -H "Authorization: Bearer $GPAGENT_API_KEY" \ "https://www.gpagent.ai/api/v1/portfolio/stats/latest"
Raw OpenAPI 3.1 spec: /api/v1/openapi.json · Markdown docs