PilotPM developers
API reference
Push signals into PilotPM from your own systems — agent-observed events and customer profile updates — and pull per-account product issues, feature requests, and summary counts into your CS tools. One auth header, an importable OpenAPI 3.1 spec. Only the endpoints on this page are part of the public, versioned API.
Authentication
Every request authenticates with a per-workspace agent token passed in the x-workspace-agent-token header. A workspace admin creates tokens under Settings → Agent tokens. The raw token (format wsk_…, 256 bits of entropy) is shown once at creation — only its SHA-256 hash is stored — and can be revoked instantly from the same screen.
- One token = one workspace. The workspace is bound server-side from the token; request bodies never carry a workspace ID, so a leaked token can never reach another tenant.
- Store the token in a secret manager (or your platform's encrypted env store) — never in source control or client-side code. Requests without a valid, unrevoked token get
401. - To rotate: generate a new token, switch your callers over, then revoke the old one. Multiple active tokens per workspace are supported, so rotation is zero-downtime.
- Scopes. Tokens carry capabilities chosen at mint time:
events:write(the write endpoints) andread(theGET /api/v1/accountsendpoints). The default mint has both; mint a read-only token for tools that should never write. Calling an endpoint without the required scope returns403. Tokens created before scopes existed have full access.
Rate limits & caps
POST /api/events: 100 requests per 60-second sliding window per token. Excess requests return429with aRetry-Afterheader (seconds) — honor it and retry.POST /api/eventsbodies are capped at 64 KB (65,536 bytes); larger payloads are rejected with413before parsing.- Individual field caps (lengths, counts, ranges) are listed per endpoint below and enforced with
422validation errors on/api/events. GET /api/v1/accounts/…: 60 requests per 60-second sliding window per token (one budget shared across the three account endpoints), same429+Retry-Aftercontract. List responses are capped at 50 rows, newest first.
/api/v1/events
Ingest an event
Push a signal observed by an out-of-process agent into the workspace's event stream (themes, competitive intel, delivery risks, call briefs). The target workspace is bound from the token — a workspace_id in the body is ignored, as is any other unknown top-level field. Request bodies are capped at 65536 bytes (64 KB) and rejected with 413 before parsing. Rate limit: 100 requests per 60-second sliding window per token; excess requests get 429 with a Retry-After header (seconds). Requires the events:write scope. Also reachable at the legacy unversioned path /api/events (identical behavior).
Auth: x-workspace-agent-token header (required).
Request body
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| agent | enum | required | intake-analyst | delivery-pm | roadmap-strategist | competitive-researcher | call-brief | Which agent persona produced this event. Must be one of the five built-in personas. |
| type | string | required | 1–64 chars | Free-form event type slug, e.g. "theme_spike" or "competitor_launch". Used for grouping and filtering. |
| severity | enum | optional | info | watch | alert; default "info" | Triage level. Defaults to info if omitted. |
| title | string | required | 1–500 chars | Human-readable headline for the event. |
| summary | string | optional | ≤ 1000 chars | Optional longer summary. |
| data | object | optional | default {} | Structured payload; shape varies per agent. The well-known fields below have enforced caps. Extra keys are accepted (passthrough). |
| citations | array<object> | required | 1–50 items | Required — every event must cite its sources. At least one citation. |
| occurred_at | string | optional | ISO 8601 UTC | When the event happened, ISO 8601 UTC with a Z suffix (e.g. "2026-07-04T09:30:00Z"; timezone offsets are rejected). Defaults to receipt time. |
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| theme | string | optional | ≤ 200 chars | |
| customer_name | string | optional | ≤ 200 chars | |
| customer_count | integer | optional | 0 – 100,000 | |
| source_count | integer | optional | 0 – 100,000 | |
| severity | string | optional | ≤ 40 chars | Agent-specific severity label inside the payload (distinct from the top-level severity enum). |
| competitor_name | string | optional | ≤ 200 chars | |
| implication | string | optional | ≤ 2000 chars | |
| estimated_arr_at_risk | number | optional | 0 – 1,000,000,000,000,000 |
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| kind | string | required | 1–40 chars | Source kind, e.g. "conversation", "slack", "ticket", "url". |
| url | string | optional | ≤ 2048 chars; valid URL | Link to the source (must be a valid URL). |
| ts | string | optional | ≤ 40 chars | Source-native timestamp or message ts. |
| id | string | optional | ≤ 200 chars | Source-native identifier. |
Example request
curl -X POST https://app.pilotpm.ai/api/v1/events \
-H 'content-type: application/json' \
-H 'x-workspace-agent-token: wsk_YOUR_TOKEN_HERE' \
-d '{
"agent": "intake-analyst",
"type": "theme_spike",
"severity": "watch",
"title": "Login failures spiking for EU users",
"summary": "14 customers reported OAuth sign-in loops in the last 24 hours.",
"data": {
"theme": "authentication",
"customer_count": 14
},
"citations": [
{
"kind": "conversation",
"id": "conv_8f3a"
}
],
"occurred_at": "2026-07-04T09:30:00Z"
}'Responses
| Status | Meaning | Example body |
|---|---|---|
| 201 | Event stored. | {
"id": "5b1e5f0a-9c1d-4a6f-8f2e-3d7c9a1b2c3d"
} |
| 400 | Body is not valid JSON. | {
"error": "invalid JSON"
} |
| 401 | Missing, malformed, revoked, or unknown token. | {
"error": "unauthorized"
} |
| 403 | Token authenticated but lacks the events:write scope (read-only tokens cannot write). | {
"error": "forbidden",
"missing_scope": "events:write"
} |
| 404 | The workspace bound to the token no longer exists (deleted after the token was created). | {
"error": "workspace not found"
} |
| 413 | Request body exceeds 65536 bytes (64 KB). | {
"error": "body too large (max 65536 bytes)"
} |
| 422 | Body failed schema validation. The issues array lists each violation (Zod issue format: code, path, message). | {
"error": "validation failed",
"issues": [
{
"code": "too_small",
"path": [
"citations"
],
"message": "citations required — every event must cite its source"
}
]
} |
| 429 | Rate limit exceeded (100 requests per 60s sliding window per token). Retry after the number of seconds in the Retry-After header.Headers: Retry-After | {
"error": "rate limit exceeded"
} |
/api/v1/customers/upsert
Upsert a customer
Idempotent upsert of a customer record, keyed on (workspace, external_id). Call it when an end-user signs in, updates their profile, or changes plan; the resulting attributes render in the inbox side panel for every conversation linked to that customer. Attributes merge shallowly (new keys win, existing keys you don't send are kept). Attribute keys are lowercased; keys longer than 60 chars are dropped; PII-shaped keys (email, name, phone, address) are dropped from attributes — use the dedicated email / name fields instead. Values may be strings (truncated to 1000 chars), numbers, booleans, null, or arrays of primitives (capped at 20 items); nested objects are silently skipped. At most 50 attributes are stored per call. Requires the events:write scope. Also reachable at the legacy unversioned path /api/customers/upsert (identical behavior).
Auth: x-workspace-agent-token header (required).
Request body
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| external_id | string | required | 1–240 chars | Your system's stable ID for this user. Leading/trailing whitespace is trimmed; the trimmed value must be non-empty. |
| string | optional | — | Optional but recommended — enables inbound-email matching. Normalized to lowercase; stored encrypted. | |
| name | string | optional | — | Optional display name. Stored encrypted. |
| attributes | object | optional | — | Flat bag of profile attributes (plan tier, platform, country, renewal risk, …). See the merge and sanitization rules in the endpoint description. |
Example request
curl -X POST https://app.pilotpm.ai/api/v1/customers/upsert \
-H 'content-type: application/json' \
-H 'x-workspace-agent-token: wsk_YOUR_TOKEN_HERE' \
-d '{
"external_id": "user_abc123",
"email": "alice@example.com",
"name": "Alice Wonder",
"attributes": {
"platform": "iOS",
"tier": "Premium",
"country": "Vietnam",
"os_version": "17.4",
"renewal_risk": "medium"
}
}'Responses
| Status | Meaning | Example body |
|---|---|---|
| 200 | Upsert succeeded. created is true when a new customer row was inserted, false when an existing one was updated. | {
"ok": true,
"id": "0d7f3b2a-1c4e-4f6a-9b8d-2e5f7a9c1b3d",
"created": true
} |
| 400 | Body is not valid JSON (invalid_body), or external_id is missing / empty after trimming (missing_external_id). | {
"error": "missing_external_id"
} |
| 401 | Missing, malformed, revoked, or unknown token. | {
"error": "unauthorized"
} |
| 403 | Token authenticated but lacks the events:write scope (read-only tokens cannot write). | {
"error": "forbidden",
"missing_scope": "events:write"
} |
| 500 | Upsert failed server-side (also returned when external_id exceeds 240 characters). | {
"error": "upsert_failed"
} |
/api/v1/accounts/{hubspotCompanyId}/issues
List an account's product issues
The account's product ISSUES: conversations whose latest inbound message the classifier tagged bug_report (the taxonomy's explicit defect bucket — crashes, errors, wrong results). Support/ops categories (billing, login, activation) are deliberately excluded. Accounts are keyed by HubSpot company id — the same key ChurnZero uses as Account External ID. Workspace is bound from the token; a company id belonging to another tenant returns 404 here. Spam and merged-duplicate conversations are excluded. Newest first, capped at 50 rows. Requires the read scope. Rate limit: 60 requests per 60-second sliding window per token.
Auth: x-workspace-agent-token header (required).
Parameters
| Name | In | Required | Constraints | Description |
|---|---|---|---|---|
| hubspotCompanyId | path | required | 1–64 chars | The account's HubSpot company id — the same value external CS tools (e.g. ChurnZero) use as the Account External ID. Up to 64 chars of [A-Za-z0-9_-]; anything else returns 400 invalid_company_id. |
| status | query | optional | open | all; default "open" | open (default) = conversations not yet resolved/closed (status open, awaiting_customer, or snoozed). all = include resolved and closed too. Unrecognized values fall back to open. |
Example request
curl https://app.pilotpm.ai/api/v1/accounts/144818714/issues \
-H 'x-workspace-agent-token: wsk_YOUR_TOKEN_HERE'Responses
| Status | Meaning | Example body |
|---|---|---|
| 200 | The account's issue conversations. | {
"ok": true,
"account": {
"hubspot_company_id": "144818714"
},
"data": [
{
"id": "5b1e5f0a-9c1d-4a6f-8f2e-3d7c9a1b2c3d",
"subject": "Scores wrong after the last update",
"category": "bug_report",
"status": "open",
"created_at": "2026-07-02T08:11:24.000Z",
"resolved_at": null
}
]
} |
| 400 | Malformed hubspotCompanyId path segment. | {
"error": "invalid_company_id"
} |
| 401 | Missing, malformed, revoked, or unknown token. | {
"error": "unauthorized"
} |
| 403 | Token authenticated but lacks the read scope. | {
"error": "forbidden",
"missing_scope": "read"
} |
| 404 | Unknown company: this workspace has never had a conversation for that HubSpot company id. (An account whose conversations are all resolved still returns 200 with empty/zero data.) | {
"error": "unknown_company"
} |
| 429 | Rate limit exceeded (60 requests per 60s sliding window per token, shared across the three account endpoints). Retry after the number of seconds in the Retry-After header.Headers: Retry-After | {
"error": "rate limit exceeded"
} |
/api/v1/accounts/{hubspotCompanyId}/requests
List an account's feature requests
The account's feature REQUESTS: conversations whose latest inbound message the classifier tagged feature_or_product. Caveat: that category also covers general product / how-it-works questions (the taxonomy doesn't split them), so treat these as an upper bound on true feature asks. Same keying, auth, exclusions, ordering, cap, status filter, and rate limit as the issues endpoint.
Auth: x-workspace-agent-token header (required).
Parameters
| Name | In | Required | Constraints | Description |
|---|---|---|---|---|
| hubspotCompanyId | path | required | 1–64 chars | The account's HubSpot company id — the same value external CS tools (e.g. ChurnZero) use as the Account External ID. Up to 64 chars of [A-Za-z0-9_-]; anything else returns 400 invalid_company_id. |
| status | query | optional | open | all; default "open" | open (default) = conversations not yet resolved/closed (status open, awaiting_customer, or snoozed). all = include resolved and closed too. Unrecognized values fall back to open. |
Example request
curl https://app.pilotpm.ai/api/v1/accounts/144818714/requests \
-H 'x-workspace-agent-token: wsk_YOUR_TOKEN_HERE'Responses
| Status | Meaning | Example body |
|---|---|---|
| 200 | The account's request conversations. | {
"ok": true,
"account": {
"hubspot_company_id": "144818714"
},
"data": [
{
"id": "0d7f3b2a-1c4e-4f6a-9b8d-2e5f7a9c1b3d",
"subject": "Can we get an export to CSV?",
"category": "feature_or_product",
"status": "resolved",
"created_at": "2026-06-21T14:03:00.000Z",
"resolved_at": "2026-06-23T09:45:12.000Z"
}
]
} |
| 400 | Malformed hubspotCompanyId path segment. | {
"error": "invalid_company_id"
} |
| 401 | Missing, malformed, revoked, or unknown token. | {
"error": "unauthorized"
} |
| 403 | Token authenticated but lacks the read scope. | {
"error": "forbidden",
"missing_scope": "read"
} |
| 404 | Unknown company: this workspace has never had a conversation for that HubSpot company id. (An account whose conversations are all resolved still returns 200 with empty/zero data.) | {
"error": "unknown_company"
} |
| 429 | Rate limit exceeded (60 requests per 60s sliding window per token, shared across the three account endpoints). Retry after the number of seconds in the Retry-After header.Headers: Retry-After | {
"error": "rate limit exceeded"
} |
/api/v1/accounts/{hubspotCompanyId}/summary
Get an account's counts-only summary
Counts-only rollup for the account, computed in one query: open/total product issues (bug_report), open/total feature requests (feature_or_product), open conversations of any category, and the timestamp of the last message in either direction (last_contact_at). Open = status open, awaiting_customer, or snoozed. Spam and merged-duplicate conversations are excluded from every count. Same keying, auth, and rate limit as the list endpoints.
Auth: x-workspace-agent-token header (required).
Parameters
| Name | In | Required | Constraints | Description |
|---|---|---|---|---|
| hubspotCompanyId | path | required | 1–64 chars | The account's HubSpot company id — the same value external CS tools (e.g. ChurnZero) use as the Account External ID. Up to 64 chars of [A-Za-z0-9_-]; anything else returns 400 invalid_company_id. |
Example request
curl https://app.pilotpm.ai/api/v1/accounts/144818714/summary \
-H 'x-workspace-agent-token: wsk_YOUR_TOKEN_HERE'Responses
| Status | Meaning | Example body |
|---|---|---|
| 200 | The account's summary counts. | {
"ok": true,
"account": {
"hubspot_company_id": "144818714"
},
"data": {
"issues_open": 2,
"issues_total": 9,
"requests_open": 1,
"requests_total": 4,
"open_conversations": 5,
"last_contact_at": "2026-07-03T18:22:41.000Z"
}
} |
| 400 | Malformed hubspotCompanyId path segment. | {
"error": "invalid_company_id"
} |
| 401 | Missing, malformed, revoked, or unknown token. | {
"error": "unauthorized"
} |
| 403 | Token authenticated but lacks the read scope. | {
"error": "forbidden",
"missing_scope": "read"
} |
| 404 | Unknown company: this workspace has never had a conversation for that HubSpot company id. (An account whose conversations are all resolved still returns 200 with empty/zero data.) | {
"error": "unknown_company"
} |
| 429 | Rate limit exceeded (60 requests per 60s sliding window per token, shared across the three account endpoints). Retry after the number of seconds in the Retry-After header.Headers: Retry-After | {
"error": "rate limit exceeded"
} |
Versioning & changelog
The current version is v1 — the endpoints documented on this page. Additive changes (new optional fields, new endpoints) ship within v1 without notice. Breaking changes ship under a new version prefix with its own spec; when that happens, the previous version keeps working for 6 monthsfrom the new version's publication, and both specs stay downloadable from this page.
- v1
READ API + token scopes: new GET /api/v1/accounts/{hubspotCompanyId}/issues, /requests, and /summary endpoints for pulling an account's product issues, feature requests, and counts into external CS tools (accounts keyed by HubSpot company id). Agent tokens now carry scopes — events:write (the write endpoints) and read (the new GET endpoints); existing tokens keep full access, and read-only tokens can be minted from Settings → Agent tokens. The write endpoints' canonical paths are now POST /api/v1/events and POST /api/v1/customers/upsert; the unversioned paths remain supported as legacy aliases.
- v1
Initial publication: POST /api/events (agent event ingestion) and POST /api/customers/upsert (customer profile upsert), authenticated with per-workspace agent tokens.