GPAgent

API Reference

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.

Sections

Vehicles

GET/vehicles

List 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"

Portfolio

GET/portfolio

List 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"
POST/portfolio

Create 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

FieldTypeRequired
namestringYes
vehicleSlugstringYes
slugstringNo
websitestringNo
sectorstringNo
stagestringNo
checkSizenumberYes
postMoneyValuationnumberNo
investmentDatestringYes
instrumentTypeSAFE | EQUITY | CONVERTIBLEYes

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"

Investor Updates

POST/portfolio/companies/{slug}/investor-updates

Upload a raw investor update

Request body

FieldTypeRequired
receivedAtstringYes
reportedPeriodstringNo
sourceemail | manual | importYes
sourceRefstringNo
fromAddressstringNo
subjectstringNo
rawBodystringYes
rawHtmlstringNo
attachmentsJsonanyNo

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"
GET/portfolio/companies/{slug}/investor-updates

List 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"
GET/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"
DELETE/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"
POST/portfolio/companies/{slug}/investor-updates/{id}/restore

Restore 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"

Metric Snapshots

POST/portfolio/companies/{slug}/metric-snapshots

Append a metric snapshot (append-only)

Request body

FieldTypeRequired
asOfstringYes
reportedPeriodstringNo
mrrUsdnumberNo
arrUsdnumberNo
cashOnHandUsdnumberNo
burnMonthlyUsdnumberNo
runwayMonthsnumberNo
postMoneyMarkedUsdnumberNo
postMoneyImpliedUsdnumberNo
lastRoundRevenueMultiplenumberNo

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"
GET/portfolio/companies/{slug}/metric-snapshots

List 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"
DELETE/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"

Customers

POST/portfolio/companies/{slug}/customers

Upsert a customer logo

Request body

FieldTypeRequired
customerNamestringYes
tierF100 | F500 | ENTERPRISE | MIDMARKET | SMB | GOV | EDU | OTHERYes
statusACTIVE | PAUSED | CHURNED | PILOT | LOST_DEALNo
dealValueAnnualUsdnumberNo
contractTermstringNo
firstSignedAtstringNo
notesstringNo

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"
GET/portfolio/companies/{slug}/customers

List 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"
DELETE/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"
POST/portfolio/companies/{slug}/customers/{id}/restore

Restore 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"
POST/portfolio/companies/{slug}/customers/{id}/churn

Mark a customer as churned

Request body

FieldTypeRequired
churnedAtstringYes
notesstringNo

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"

Data Gaps

POST/portfolio/companies/{slug}/data-gaps

Create a data gap report

Request body

FieldTypeRequired
gapTypeATTACHMENT_UNREAD | LINK_UNREAD | METRIC_MISSING | CUSTOMER_AMBIGUOUS | PERIOD_UNCLEARYes
descriptionstringYes
updateIdstringNo
suggestedFieldstringNo
hintstringNo

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"
DELETE/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"
GET/portfolio/data-gaps

List 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"
POST/portfolio/data-gaps/{id}/resolve

Resolve 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

FieldTypeRequired
resolvedValueanyYes
notesstringNo

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"
POST/portfolio/data-gaps/{id}/dismiss

Dismiss 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

FieldTypeRequired
notesstringNo

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"
POST/portfolio/data-gaps/bulk-resolve

Resolve 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

FieldTypeRequired
resolutionsarrayYes

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"
POST/portfolio/data-gaps/bulk-dismiss

Dismiss up to 100 data gaps at once

Per-item partial success. Hard cap of 100 per batch.

Request body

FieldTypeRequired
gapIdsarrayYes
notesstringNo

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"

News

GET/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"
DELETE/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"
POST/portfolio/news

Bulk-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

FieldTypeRequired
itemsarrayYes

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"
POST/portfolio/news/maintenance

Self-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"

Stats

DELETE/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"
POST/portfolio/stats/recompute

Recompute 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

FieldTypeRequired
vehicleSlugstringNo
vehicleIdstringNo

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"
GET/portfolio/stats/latest

Get 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