Provisioning API
Contents
If you're onboarding users from your own product into PostHog, this API lets you skip the signup form. Call three endpoints and you get back a project token, a personal API key, and a host URL – everything you need to start sending events into the user's project. The user still gets a welcome email with a link to set their password and access their dashboard.
It's intended for partners and platform integrations. If you're integrating your own app with your own PostHog account, you probably want OAuth or a personal API key instead.
Jump to the full Node.js example to see the complete flow in code.
How it works
The flow uses OAuth 2.0 with PKCE (Proof Key for Code Exchange), so there are no shared secrets to manage. Your application is identified by a metadata document hosted on your domain.
Set up as a partner
Host a CIMD metadata document
PostHog uses Client ID Metadata Documents (CIMD) for partner registration. There's no signup form to fill out – you host a JSON document at an HTTPS URL on your domain, and that URL becomes your client_id.
Create a JSON file at a stable HTTPS URL, for example https://yourapp.com/.well-known/posthog-client.json:
Requirements:
client_idmust exactly match the URL where this document is hosted.redirect_urisis required and must contain at least one HTTPS URI. This is where PostHog redirects existing users during the consent flow.logo_uri(optional) must be HTTPS if provided.token_endpoint_auth_methodmust be"none"(CIMD clients are public clients, no client secret).- The document must be served with
Content-Type: application/jsonand be under 5 KB. - The URL must use HTTPS, include a path component, and must not contain query parameters or fragments.
PostHog fetches and caches this document automatically.
Subsequent requests reuse the cached version and refresh it in the background based on your Cache-Control: max-age header (clamped between 5 minutes and 24 hours, default 1 hour).
Once your metadata document is live, you can start calling the API. The first request auto-registers your app and returns HTTP 202; retry after a few seconds.
Link your partner app to a PostHog organization (optional)
By default, a CIMD partner app is unverified and capped at 10 account requests per hour. You can link the app to a PostHog organization with a verification token to raise that to 100/hour and surface the partner integration to that org's admins.
In PostHog, go to Organization settings → CIMD verification tokens and click Create token. Copy the
phvt_…value – it's only shown once.Add a
posthog_verification_tokenfield to your CIMD metadata document:JSONThe next time PostHog refreshes the metadata document, the app is linked to the matching organization and the rate limit is bumped.
The token is only used to prove ownership of the partner app – it isn't sent on API requests. You can rotate or revoke a token at any time from the same settings page. Revocation clears the link on the next metadata refresh, so a leaked or stale token can't keep an app linked to your org.
API reference
All endpoints are on https://us.posthog.com (US region) or https://eu.posthog.com (EU region).
Every request must include the API-Version: 0.1d header.
Step 1: Create an account
Create a PostHog account for a user by email. If the user is new, PostHog creates the account and returns an authorization code immediately. If the user already exists, the response tells you to redirect them for consent.
Generate a PKCE code verifier and challenge before making this request:
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Your unique request ID (for idempotency) |
email | string | Yes | User's email address |
name | string | No | User's full name |
client_id | string | Yes | Your CIMD metadata URL |
code_challenge | string | Yes | Base64url-encoded SHA-256 hash of your code verifier (43-128 chars) |
code_challenge_method | string | Yes | Must be "S256" |
scopes | list | No | OAuth scopes to request for the access token. See available scopes. |
configuration.region | string | No | "US" (default) or "EU" |
configuration.organization_name | string | No | Organization name (defaults to "Partner (email)") |
New user response (HTTP 200):
The user receives a welcome email with a link to set their password and access their dashboard.
Existing user response (HTTP 200):
When type is requires_auth, redirect the user to the provided URL. After they approve, PostHog redirects them to your redirect_uris with a code query parameter that you use in step 2.
Deep linking
The requires_auth flow isn't only for first-time onboarding. Use the same handshake every time you want to deep-link a user from your own application into PostHog. A common case is an "Open in PostHog" button on the project a user has already connected: each click should run the handshake again, not point at a static PostHog URL.
For each click, your backend calls /account_requests with the user's email, then you redirect them to the returned requires_auth.url. When the logged-in PostHog session matches the email you sent, PostHog completes the OAuth handshake and lands the user in their project.
If the session is for a different PostHog account, PostHog shows an "Account mismatch" page with a one-click Log out and continue as <expected email> button that resumes the flow after re-login. To smooth this case even further – so users can self-diagnose before they click – consider also surfacing the expected PostHog email in your own UI, for example a button labeled "Open in PostHog as user@example.com".
Avoid linking directly to project URLs
Pointing a button straight at https://us.posthog.com/project/<id> skips the handshake and removes PostHog's ability to route the user safely:
- If the user is logged into PostHog with a different email, they hit a permission error or 404 on a project their current account can't access.
- If they're logged out, they get the generic PostHog login page with no context about which email to use.
Going through /account_requests keeps routing scoped to the email you sent.
Step 2: Exchange the code for tokens
Exchange the authorization code for an access token and refresh token. The token endpoint uses standard application/x-www-form-urlencoded encoding.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be "authorization_code" |
code | string | Yes | The authorization code from step 1 |
code_verifier | string | Yes | The original PKCE code verifier (must match the challenge from step 1) |
Response (HTTP 200):
Authorization codes expire after 5 minutes and can only be used once. Access tokens expire after 1 hour. Use the refresh token to get new tokens.
Token endpoint errors use the standard OAuth 2.0 format:
Step 3: Provision a project
Use the access token to provision a PostHog project and get credentials.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
service_id | string | No | The plan to provision. "analytics" (default) provisions a standard project. "free" and "pay_as_you_go" set the billing plan explicitly. |
label_prefix | string | No | Label prefix for the personal API key shown in PostHog, up to 25 characters. The key is labeled {label_prefix} - {team_name}. If omitted, empty, or whitespace-only, the key is labeled with just the team name. |
configuration.project_name | string | No | Project name (defaults to "Default project") |
Response (HTTP 200):
Response fields:
| Field | Description |
|---|---|
api_key | The project token (starts with phc_) – use this to initialize PostHog SDKs |
host | The API host (https://us.posthog.com or https://eu.posthog.com) |
personal_api_key | A personal API key scoped to this project – use this for the PostHog API |
Rotate project credentials
Rotate the project token and create a new personal API key for an existing provisioned project:
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
label_prefix | string | No | Label prefix for the new personal API key shown in PostHog, up to 25 characters. The key is labeled {label_prefix} - {team_name}. If omitted, empty, or whitespace-only, the key is labeled with just the team name. |
The response has the same shape as the project provisioning response and includes the rotated api_key, host, and new personal_api_key.
Refresh tokens
Access tokens expire after 1 hour. Use the refresh token to get new credentials:
Response (HTTP 200):
Each refresh token is single-use. The response includes a new refresh token for subsequent refreshes.
Available scopes
The scopes field in the account request controls what permissions the access token receives. If omitted, a default set of scopes is granted. Available scopes:
| Scope | Description |
|---|---|
customer_journey:read | Read customer journey data |
query:read | Execute read-only queries |
conversation:read | Read PostHog AI conversations |
conversation:write | Create and update PostHog AI conversations |
experiment:read | Read experiments |
feature_flag:read | Read feature flags |
insight:read | Read insights |
organization:read | Read organization details |
person:read | Read person data |
project:read | Read project settings |
ticket:read | Read tickets |
ticket:write | Create and update tickets |
user:read | Read user information |
hog_flow:read | Read Hog flows |
hog_flow:write | Create and update Hog flows |
What the user gets
When you provision a new account, the user receives:
- A welcome email with a link to set their password.
- Full dashboard access at us.posthog.com (or eu.posthog.com for EU).
- The PostHog free tier across all products – no credit card required.
Your integration gets back the project token and host, so you can start sending events the moment the API call returns.
Error handling
Provisioning endpoints (account_requests, resources) return errors in this format:
The token endpoint uses the standard OAuth 2.0 error format instead:
Common error codes:
| Code | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Missing or invalid field |
unauthorized | 401 | Authentication failed |
forbidden | 403 | Partner not authorized for this action |
expired | 400 | Account request has expired |
invalid_grant | 400 | Authorization code is invalid or expired (token endpoint) |
invalid_label_prefix | 400 | label_prefix is not a string, is longer than 25 characters after trimming, or contains control or Unicode format characters |
invalid_scope | 400 | Unrecognized scope requested |
rate_limited | 429 | Rate limit exceeded |
account_creation_failed | 500 | Server error during account creation |
Rate limits
All provisioning endpoints are rate limited.
CIMD registration (first request from a new client_id):
- 5 requests per minute per IP (burst)
- 10 requests per hour per IP (sustained)
- 100 new client registrations per hour globally
- 5 new client registrations per domain per hour
Account requests (per partner, per hour):
- 10/hour for unverified CIMD partners
- 100/hour for partners linked to a PostHog organization via a verification token
Email team-growth@posthog.com if you need a higher limit for production use.
Full example
Here's a complete example in Node.js: