Management API reference#
This reference lists HTTP operations, required permissions, and response codes. For a task walkthrough, start with projects, people, queries, or pipelines.
The running server publishes its OpenAPI contract at GET /openapi.json. The CLI and MCP use the same operation IDs and authorization rules. The project analytics schema has its own endpoint.
Authentication and scope#
Send a Bearer credential issued for ISSUER/api/v1. A person can authorize the CLI with email sign-in. A machine can use an API-audience service key or exchange its client credentials for a short-lived OAuth access token.
On first startup, PORCUPINE_BOOTSTRAP_TOKEN creates an instance:admin
principal with * project scope. The OAuth client ID bootstrap exchanges its
original secret. Subsequent startups preserve state; changing the environment
does not reset credentials. The alias stays pinned to that first administrator.
Each principal has permissions and project IDs. * includes all projects.
instance:admin grants all operations and requires * scope. An access token's
issued scope and project snapshot intersect current parent grants on every
request. Operators can choose email accounts for interactive API and MCP access, or upstream OIDC for read-only MCP. Each deployment selects one interactive login method. Authentication
runs before request validation. HTTP and MCP share operation policies
and project checks, with separate API and MCP token resources. The CLI generates its commands from the same OpenAPI operations.
With an issuer configured, legacy API and MCP Bearer keys are rejected by default. Explicit legacy-key allowance is a migration setting. Without any OAuth configuration, the server retains legacy authentication for offline use. Public PostHog ingestion keys are separate and remain valid for capture.
Operations#
query-events accepts an optional use_cache Boolean. Omitted or true allows
a 60-second ClickHouse result cache. false bypasses both cache directions and
reads currently projected data. See the analytics reference
for mutation invalidation and delivery consistency.
| Operation | Required authority | Success |
|---|---|---|
GET /api/v1/status |
management:read; no project data or secrets in the instance report |
200, including a degraded report |
GET /api/v1/projects |
management:read; results restricted to permitted projects |
200 |
POST /api/v1/projects |
management:write and events:write, both with * scope |
201 |
PATCH /api/v1/projects/{id} |
management:write for this project |
200 with the updated project |
DELETE /api/v1/projects/{id} |
instance:admin |
204 after cleanup |
GET /api/v1/access-keys |
management:read; results restricted to keys whose entire project scope the caller holds |
200 |
POST /api/v1/access-keys |
management:write; every requested permission and project must be held by the caller |
201 |
DELETE /api/v1/access-keys/{id} |
instance:admin |
204 |
POST /api/v1/oauth-clients |
management:write; requested grants cannot exceed the caller |
201 |
GET /api/v1/oauth-clients |
management:read; complete project scope required for each returned client |
200 |
DELETE /api/v1/oauth-clients/{id} |
instance:admin; last administrator protected |
204 |
POST /api/v1/oauth-clients/{id}/api-keys |
instance:admin; service key must fit the machine client's grants |
201 |
GET /api/v1/oauth-grants |
instance:admin |
200 |
DELETE /api/v1/oauth-grants/{id} |
instance:admin |
204 |
POST /api/v1/humans |
instance:admin on an email-enabled instance |
201 |
GET /api/v1/humans |
instance:admin on an email-enabled instance |
200 |
PUT /api/v1/humans/{id} |
instance:admin on an email-enabled instance |
200 |
DELETE /api/v1/humans/{id} |
instance:admin on an email-enabled instance |
204 |
GET /api/v1/projects/{id}/events?limit=100 |
events:read and access to the project |
200 |
GET /api/v1/projects/{id}/quarantined-events |
events:read and access to the project |
200 |
POST /api/v1/projects/{id}/quarantined-events/{event_id}/retry |
events:edit and access to the project |
202 |
GET /api/v1/projects/{id}/schema |
events:read and access to the project |
200 |
GET /api/v1/projects/{id}/catalog |
events:read and access to the project |
200 |
POST /api/v1/projects/{id}/query |
events:read and access to the project |
200 |
GET /api/v1/projects/{id}/events/{uuid} |
events:read and access to the project |
200 |
PATCH /api/v1/projects/{id}/events/{uuid} |
events:edit and access to the project |
202 |
DELETE /api/v1/projects/{id}/events/{uuid} |
events:delete and access to the project |
202 |
GET /api/v1/projects/{id}/event-mutations/{mutation_id} |
events:read and access to the project |
200 |
For managed SQL exports, see the export job operations. The pipeline operations configure recurring deliveries. Historical imports use HTTP or the CLI for the entire import, including binary chunk uploads. The running server publishes its enabled operations at /openapi.json.
Project creation accepts {"name":"Example"} and returns a mixed project plus
a one-time public ingest_token with events:write for that project. With OAuth
enabled, send {"name":"Trusted service","ingestion_mode":"server_only"} to
require authenticated backend writes. The response omits the public key. The
creation operation requires the same permissions for both modes. See the
ingestion policy.
rename-project accepts {"name":"Research"}. Names must contain 1–200 UTF-8 bytes after trimming whitespace. The response contains the updated project. Existing SDKs, dashboards and pipelines continue to address the project by ID.
delete-project accepts the project ID in the path and takes no request body. An instance administrator can use it to permanently remove a project's queued and analytical events, metadata, pipeline definitions and Porcupine-managed exports. Copies already delivered to customers' webhook or S3 destinations and existing backups are outside the deletion scope. Bucket lifecycle rules control retention of noncurrent object versions. See export cleanup. If a storage dependency is unavailable and the request returns 503, retry the same ID. The server also retries pending cleanup every minute and on restart. A repeat request for a completed deletion returns 204. An unknown ID returns 404.
Deleting a project removes it from access grants. Changing a parent grant also revokes its derived keys and sessions under the usual credential rules. A user or service with access to a surviving project may need to reconnect or obtain a replacement key.
OAuth client creation accepts this body:
{
"name": "Analytics reader",
"permissions": ["events:read", "management:read"],
"projects": ["PROJECT_ID"]
}
The response contains client_id, one-time client_secret, name,
permissions, projects, and created_at. The secret is for token exchange,
not direct API authorization. Names contain 1 to 200 UTF-8 bytes and cannot be whitespace only.
Creating independent access keys or OAuth clients also requires an instance administrator or an eligible machine credential. A project-scoped human session or static service key with management:write can rename its project but cannot issue independent credentials. Permissions and projects must each contain at least one entry. Project IDs
must exist unless the scope is *.
OAuth client listing returns {"enabled":true,"oauth_clients":[...]} without
secrets or hashes. Items contain client_id, name, grants, creation time, and
nullable revocation time. The bootstrap alias is not listed as a registered
client. See OAuth client management.
In OAuth mode with legacy keys disabled, access-key creation is restricted to
permissions: ["events:write"] and exactly one project. It returns the one-time
token used by public PostHog ingestion. Use an OAuth machine client or a service key for service and agent access. Legacy mode retains the older scoped-key behavior.
Key listing returns {"access_keys":[...]}. Each item contains id, name,
permissions, projects, created_at, and revoked_at. Active keys have
revoked_at: null. Revoked keys remain visible within scope. A key spanning
projects A and B is omitted entirely from a caller restricted to A.
Client or principal revocation disables future authentication, including all
access tokens issued to it. Ordinary already-authenticated requests are not
automatically canceled. Exports also recheck creator authority while running. Revoking the last active instance administrator
returns 409. The check and update share a SQLite write transaction.
Access tokens expire; public ingestion keys and client secrets have no expiry
or dedicated rotation endpoint. Rotate a client by creating a replacement and
revoking the old principal.
The event-read operation returns {"events":[...]} from the pending SQLite
queue, newest first. limit defaults to 100 and accepts 1 through 1,000.
Acknowledged delivery removes payloads from that queue, so this endpoint is
queue inspection rather than historical event access.
Quarantined delivery repair#
list-quarantined-events lists accepted originals retained after confirmed
row-specific ClickHouse rejection. It returns metadata only. Event payloads,
properties, visitor IDs, and raw ClickHouse errors are absent.
GET /api/v1/projects/{id}/quarantined-events?limit=100&after_event_id=0
returns events, limit, after_event_id, and an optional
next_after_event_id. Each event contains event_id, uuid, error_code,
quarantined_at, and received_at. Event IDs and cursors are decimal strings
to preserve 64-bit precision. Confirmed rejection codes are 321 and 469.
Pages sort by increasing event ID. limit defaults to 100 and accepts 1 through
1,000. Pass next_after_event_id as the next request's after_event_id.
Pagination uses an event-ID cursor, without offsets. The listing reflects current
quarantine state rather than a snapshot. Restart from cursor 0 to include older
originals that entered quarantine after an earlier page.
After repairing the downstream constraint or deployment configuration,
retry-quarantined-event restores the original's delivery eligibility:
POST /api/v1/projects/PROJECT_ID/quarantined-events/EVENT_ID/retry
The request has no body. A 202 response contains
{"event_id":"EVENT_ID","requeued":true}. Retrying an original already eligible
for delivery returns requeued:false. A missing or already acknowledged original
returns 404, including an event ID belonging to a different project.
Retry preserves the original event ID, UUID, payload, revision, and receipt time. It does not acknowledge ClickHouse delivery, remove the accepted payload, or free queue capacity. The normal delivery worker processes the event after retry. No service restart or pause is needed. A remaining row-specific rejection can quarantine the event again. Inspect project delivery status and quarantine metadata after repair. Corrections and deletions retain their existing mutation acknowledgement requirements.
Both operations are available through porcupine_call with the same operation
IDs, project checks, and permissions as HTTP. porcupine_docs exposes listing to
readers and retry to callers with events:edit.
The analytics API reference describes get-event-schema,
get-event-catalog, and query-events, including SQL examples, fixed quotas,
and eventual consistency. All three require one permitted project per request.
The event mutation reference describes current-event reads, full
replacement corrections, deletion tombstones, revision checks, idempotency, and
mutation status. A 202 confirms durable acceptance, not analytical visibility.
Storage tiering is deployment-only and has no management endpoint.
Historical PostHog imports#
The import reference covers manifest creation, binary chunk upload, status, resume, cancellation and import permissions. These operations use the API audience.
Delivery pipeline management#
Project routes under /api/v1/projects/{id}/pipelines configure webhooks and
scheduled S3 delivery. Reads require pipelines:read plus events:read; all
writes and retries require pipelines:write plus events:read. These are explicit
egress grants, separate from management permissions and event-reading authority.
The pipeline reference lists all ten operation IDs and routes. Lists contain bounded metadata; a detail read includes the nonsecret rule. Destination endpoint URLs, credentials, event payloads, and remote response bodies are never returned. The operator must configure the encryption key before creation is available. Restore pauses destinations until an authorized caller explicitly resumes them after review.
Errors and limits#
Error responses use {"error":"message"}. Domain and authentication errors
use application/json. Huma validation errors use application/problem+json
with the same envelope.
| Status | Meaning |
|---|---|
400 |
Malformed JSON, invalid permission or project, or a domain validation failure |
401 |
Missing, invalid, expired, revoked, or wrong-resource access token |
403 |
Missing permission, project outside scope, or attempted privilege escalation |
404 |
Revocation target or retained event does not exist, or the target is already revoked |
409 |
Last administrator protection, stale revision, pending mutation, or idempotency conflict |
410 |
Requested event has been deleted |
413 |
Request body exceeds 1 MiB |
415 |
Unsupported body encoding or content type |
422 |
Request schema validation failed, including wrong types, required fields, or invalid limits |
429 |
Analytics concurrency or ClickHouse resource limit exceeded |
500 |
Storage or internal error |
503 |
Analytics storage, reader provisioning, or delivery-status storage unavailable |
Management requests with a body use uncompressed JSON, except for binary PostHog import chunks. Browser CORS configuration
applies to ingestion endpoints only. events:edit and events:delete authorize
implemented mutation endpoints. identity:write remains a reserved permission.
The access design describes analytics, SQL, and
MCP boundaries.
Source-bound ingestion credentials#
Ingestion credentials accept an optional source_id for producer identity and retry deduplication. A bound writer needs events:write for one concrete project. The source is case sensitive, starts with a letter or digit, and accepts letters, digits, dots, underscores, colons, and hyphens up to 128 ASCII characters. The exact value posthog is reserved for legacy PostHog capture. The source appears in credential metadata. A source-bound parent issuing another writer delegates its own source.