Surveys
NPS, CSAT, and CES surveys with hosted public responses, closed-loop automation that turns detractors into save tasks, and lifecycle triggers that fire surveys at the right moment.
Surveys
Pact surveys measure customer sentiment with the three standard instruments —
NPS, CSAT, and CES — and then act on the results. Responses are
collected through hosted public pages, scored automatically, and fed into
closed-loop workflows. Admin CRUD lives at /v1/surveys; public responses come
in through /public/surveys. Backing tables are surveys and
survey_responses (alembic 0146).
Survey types
Each type has a fixed scoring scale baked into SURVEY_TYPES
(api/routes/surveys.py):
| Type | Scale | Default question |
|---|---|---|
nps | 0–10 | "How likely are you to recommend us to a friend or colleague?" |
csat | 1–5 | "How satisfied are you with your experience?" |
ces | 1–7 | "How satisfied are you with your experience?" (effort framing) |
GET /v1/surveys/types
returns the catalog with each type's score_min, score_max, default question,
and default follow-up prompt — the data behind the "+ New Survey" type picker.
Create and manage
GET /v1/surveys → list (filter by status, type, q)
POST /v1/surveys → create from a type template
GET /v1/surveys/{survey_id} → get one
PATCH /v1/surveys/{survey_id} → update name/question/follow-up/status/channels
DELETE /v1/surveys/{survey_id} → soft delete
A survey has a name, a question, an optional open-text follow_up_prompt, a
status, and a list of channels (defaults to ["email"]). Creating from a
type pre-fills the question and follow-up from the template. Each survey gets a
public_id (UUIDv4) — that is what the public response endpoint resolves
against, never the internal integer id.
Collecting responses
Respondents answer through a hosted page at /public/surveys/{public_id}, with
IP rate-limiting and a respond endpoint that records the score, optional
comment, and a scoring band (promoter/passive/detractor for NPS). Response
identifiers are hashed for privacy.
Closed-loop automation
The real value is what happens after a response lands. Rules in
core/surveys/closed_loop.py fire immediately on submit, and are toggled at
/v1/survey-rules:
- Detractor → CS task (
detractor_to_task) — an NPS score of 0–6 with an attributed account auto-creates a pending save task for the account owner, with the verbatim comment attached as evidence. - Promoter → referral CTA (
promoter_to_referral) — an NPS score of 9–10 queues a referral event for marketing follow-up.
Rules default ON
Every rule kind is enabled by default (DEFAULT_ENABLED), so a new tenant gets
the closed loop without discovering the toggle first. A rule kind with no row in
survey_rules is treated as enabled. Toggle any rule at
PATCH /v1/survey-rules/.
Lifecycle triggers
Beyond the reactive rules, Pact ships lifecycle triggers that decide when to
send a survey (core/surveys/lifecycle.py), also default-on:
post_onboarding_t7— 7 days after onboardingpost_close_won_t14— 14 days after a deal closes wonpre_renewal_t30— 30 days before renewalpost_support_case_close— after a support case closes
Together these give a new tenant a standard NPS/CSAT cadence out of the box.