Implementing API Surveys
PostHogOfficialPublished by PostHog
- Skill
- Tool integration
- Data and AI
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill guides you through implementing headless API surveys in your application. Unlike standard surveys, API surveys are defined in PostHog but rendered by your own UI components. This approach gives you full control over the survey's appearance while ensuring that interaction events are captured correctly in PostHog. The process covers fetching survey definitions, implementing the event capture contract, managing survey state, and verifying that responses are correctly recorded.
What it does
- Fetches survey definitions from PostHog using the survey ID.
- Provides implementation patterns for browser-based (posthog-js) and server-side/mobile environments.
- Ensures correct event capture for survey shown, sent, and dismissed actions.
- Guides the implementation of the required event property contract for accurate data attribution.
- Includes logic for managing survey visibility and deduplication using submission IDs.
- Verifies successful implementation by checking response data and stats via PostHog.
When to use it
- When you need to build a custom survey UI that matches your application's design system.
- When you are implementing a headless survey of type 'api' in a web, server, or mobile environment.
- When you need to ensure that survey events are captured and attributed correctly in PostHog.
- When you need to verify that survey responses are being recorded and counted accurately.
Advantages
- Full control over the survey's UI and user experience.
- Seamless integration with existing application components.
- Ensures data integrity by following the strict PostHog event contract.
- Provides a clear path for verification to avoid silent data collection issues.
Usage examples
- “Help me implement the API survey with ID 123 in my React application.”
- The agent will guide you through fetching the survey definition, setting up the posthog-js listeners, creating the UI component, and implementing the capture functions for survey events.
- “I need to wire up a headless survey in my Node.js backend.”
- The agent will provide the server-side logic for capturing survey events, setting the necessary person properties, and handling survey eligibility.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/posthog/implementing-api-surveys/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip implementing-api-surveys.zip -d .agents/skills
Documentation by PostHog
Implementing API surveys
An api survey is only a definition in PostHog. PostHog does not render it. The application renders the questions and captures the survey events. If the events or their properties are wrong, responses are missing, double-counted, or unattributed.
1. Get the survey
Fetch it with posthog:survey-get by id. Note each question's id, type, question, choices, scale and optional, plus current_iteration. Never invent question ids.
If the survey does not exist yet, create it first with the creating-surveys skill (type api). Design guidance (short, rating or choice first, open text second) lives there too. Do not restate it here.
2. Read the user's codebase
Before writing UI, find:
The framework and the existing component library or design system. Build the survey from those components, not a generic modal.
Where PostHog is initialized, and the installed SDK and version (
package.jsonor lockfile, or the server language's manifest).Where the survey should appear. Let the app own the trigger: leave the survey's event, URL and selector
conditionsempty, and run the lookup below at the moment the survey should appear.getActiveMatchingSurveysonly returns a survey with event conditions after that event fires, and nothing runs the lookup again at that point.
Pick the path:
Browser app with posthog-js: continue below.
Server-rendered pages, other SDKs (Python, Node, Go, Ruby, PHP), or custom mobile UI: read server-and-mobile.md, then return to step 5. Mobile SDKs render only popover surveys natively, so a custom mobile UI follows the server pattern.
3. Browser contract (posthog-js)
Require posthog-js 1.249.2 or newer. Older versions do not set the person properties and seen state on capture. Tell the user to upgrade rather than adding them by hand.
The lookup only works once both flags and the surveys bundle have loaded, because targeting is evaluated with flags and getActiveMatchingSurveys does nothing before surveys load. Register one retry for each at startup, and guard against rendering twice, since onFeatureFlags also runs on every flag reload (after identify, and after each response).
let triggered = false
let rendered = false
function tryShowSurvey() {
if (!triggered || rendered) return
posthog.getActiveMatchingSurveys((surveys) => {
const survey = surveys.find((s) => s.id === SURVEY_ID)
if (!survey || rendered) return
rendered = true
renderSurvey(survey)
})
}
// Register once at startup, not per trigger.
posthog.onFeatureFlags(tryShowSurvey)
posthog.onSurveysLoaded(tryShowSurvey)
function showSurveyIfEligible() {
triggered = true
tryShowSurvey()
}
// Call when the survey closes, after the final send or a dismiss.
function onSurveyClosed() {
triggered = false
rendered = false
}Call showSurveyIfEligible() from the app's trigger, such as after a checkout completes, or right after setup if the survey belongs on page load. Call onSurveyClosed() when the survey closes, so a survey that can repeat shows again on the next trigger. A survey that cannot repeat stays hidden, because posthog-js marks it as seen. In a single-page app, register the two listeners where the app initializes PostHog, and call the functions they return to unsubscribe if that code can unmount. SURVEY_ID is the id from step 1. renderSurvey mounts the component you build from the app's own UI, and that component calls the handlers below. getActiveMatchingSurveys applies the survey's targeting for this person, so render only what it returns.
Capture three events. answers maps each question id to its answer, and holds every answer given so far in this submission. Exact property names and value shapes are in event-contract.md.
function onShown(survey) {
posthog.capture('survey shown', {
$survey_id: survey.id,
$survey_iteration: survey.current_iteration,
})
// posthog-js reads this key for the survey wait period; only its own renderer writes it.
try {
localStorage.setItem('lastSeenSurveyDate', new Date().toISOString())
} catch {}
}
function onSubmit(survey, answers, submissionId, completed) {
posthog.capture('survey sent', {
$survey_id: survey.id,
$survey_iteration: survey.current_iteration,
$survey_submission_id: submissionId,
$survey_completed: completed,
$survey_questions: survey.questions.map(({ id, question }) => ({ id, question })),
...Object.fromEntries(
Object.entries(answers).map(([questionId, value]) => [`$survey_response_${questionId}`, value])
),
})
}
function onDismiss(survey) {
posthog.capture('survey dismissed', {
$survey_id: survey.id,
$survey_iteration: survey.current_iteration,
})
}Rules that cause silent data problems:
Generate one
$survey_submission_id(crypto.randomUUID()) when the respondent starts, and reuse it for every send of that submission. PostHog deduplicates and bills responses by this id. Events without it each count as a separate response.$survey_completedisfalsefor a partial send andtruefor the final send.Send every answer so far on each send, not only the newest one. PostHog keeps the latest event per submission id.
Ratings go as a number, as posthog-js sends them. Multiple choice goes as an array of strings. Send a
$survey_response_<question_id>key only for answered questions.Dismiss means close without answering. Do not send
survey dismissedafter a finalsurvey sent.
4. Do not set seen state by hand in the browser
posthog-js sets $survey_responded/<id>[/<iteration>] or $survey_dismissed/... and the local seen flag when it captures survey sent or survey dismissed. That is what stops the survey being offered again. Adding those properties yourself duplicates it.
5. Verify the first response
Ask the user to trigger the survey once in the real app and submit it. Then:
Call
posthog:surveys-responses-listfor the surveyid. Rows resolve question text server-side. Confirm the answer appears under the right question, and that multiple choice answers arrive as a list.Call
posthog:survey-statsfor the surveyid. Confirmshownandsentcounts moved, and that one submission is one response, not two.If nothing arrives, check the project token and that
survey sentappears in the live events, then followdebugging-surveys. Do not guess.
Report what you implemented, where the trigger lives, and the verified response.
Files
- ATTRIBUTION.mdAdded by AutoGineer867 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md7.2 KiB
- references/event-contract.md2.8 KiB
- references/server-and-mobile.md4.3 KiB
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- PostHog
- Publisher
- PostHog (Official)
- Source
- PostHog/skills · skills/omnibus/implementing-api-surveys · commit a5d30e7
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by PostHog.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download implementing-api-surveys.zip