Before you start
- Every Surveys endpoint needs a secret key (
gt_live_). Publishable keys (gt_pub_) are refused with403. - Reading needs the
read:surveysscope. Creating, editing, launching, inviting, recording and changing rules needwrite:surveys. See Authentication. - Lists use
limit(default 50, max 100) andoffset. There is no cursor. - Each key can make 60 requests per minute. Past that, the API returns
429with the coderate_limit_exceeded. Back off and retry. - Errors use the standard envelope. A body that isn’t valid JSON is
400. A value that fails its own field’s validation is422. Rules that compare several fields are different: onPATCH /surveys/{id}and when you launch a survey, a failure (a missing first question, an NPS scale outside 0-10, a CSAT or CES scale outside 1-5 or 1-7, promotional copy,showWhenrules) returns400bad_requestwith a generic message and no field detail. Answers that don’t fit their questions, or asubjectthat can’t be linked, onPOST /surveys/{id}/responsesare422. The session endpoint returns422for any invalid or malformed body. Rules the survey enforces, such as editing locked questions, are400. See Errors.
Measures and scales
A survey has onetype, which the API returns as measure.
For
nps, csat and ces, the first question carries the score. It must be a required rating or scale question. A survey can have up to 20 questions. Each has a type:
Choice questions need at least two different
options. A multiple-choice question can set preset: "pmf" to ask the product-market fit question with its four standard answers.
A question can use showWhen to appear only after a matching earlier answer, such as “ask why” when the first score is 6 or lower. showWhen names an earlier question by its zero-based index (question, the first question by default), an operator and a value. The operators depend on the question it reads:
Surveys must stay purely feedback. Copy with promotional phrases such as “discount code” or “limited time” is rejected, so the survey can be sent without marketing consent.
Bands
Every response gets aband. Use it to route feedback without knowing the scale.
A response with no score, and every response to a
custom survey, is unscored.
Survey lifecycle
1
Create a draft
POST /surveys creates a survey with status: "draft". Nothing is collected yet.2
Launch it
POST /surveys/{id}/status with { "status": "active" } validates the survey again and starts collection. GainTrace sends the survey.launched webhook.3
Close or reopen it
Send
closed to stop collecting and active to start again. A launched survey can’t go back to draft.What you can still change
PATCH /surveys/{id} edits any part of the survey while it is a draft. Once a survey is active, or has any responses, three things lock: type, questions and settings.anonymous. Changing one returns 400 with the message “Duplicate this survey to change questions or anonymity after launch.” This keeps old answers meaningful. Name, cooldown, thank-you copy, reminders, send window, in-app rules and the other settings stay editable.
When you send settings, each top-level key you include replaces that key and the rest are kept. Nested objects such as reminders are replaced as a whole.
Channels
A response arrives through one channel. Thesurvey_response webhooks name it in channel.
The values
link, qr and kiosk are historical only. They label answers collected before public links, QR codes and kiosks were removed, and no new answer arrives through them. A survey object has no links field: every answer comes through a personal entry point.
Invitation emails link to short personal addresses such as https://app.gaintrace.com/s/aB3dE5gH7jK9. While the white-label add-on is active and your survey domain is verified, they use your own domain. Links already sent keep working. The REST API stays on the GainTrace address.
Answers that arrive from an imported survey tool (see Import from other survey tools) use the channel import on webhooks.
Signed invitation links and identified in-app sessions tie each answer to a person and company. A survey page opened without a personal link shows “This survey only takes answers from a personal invitation. Ask the team that sent it for your link.” and records nothing.
A survey can name up to 10 hiddenFields, such as plan. When a personal link carries that parameter, for example ?plan=pro, the value is saved with the response. Parameters the survey doesn’t name are dropped.
Record responses from your own system
UsePOST /surveys/{id}/responses when your product, support tool or bot already collected the answer. The response goes through the same validation, attribution, signals, Flows, timeline and webhooks as an emailed survey.
- The survey must be
active. A draft or closed survey returns404. personIdmust be an active person in your workspace.answersare keyed by question index:q0,q1and so on. Required questions that are visible must be answered, and each value must fit its question.scoreis the first question’s score. If you also sendanswers.q0, the two must match.hiddenis an optional object of hidden-field values, such as{ "plan": "pro" }. Only names the survey lists inhiddenFieldsare saved.subjectis an optional record the answer is about. See Link a response to a record.
Idempotency
externalId is your own stable id for the answer, up to 255 characters. GainTrace turns it into a deterministic key for that survey. If you send the same externalId for the same survey again, you get the first response back with duplicate: true and status 200. The new body is ignored and nothing is created twice. The first call returns 201 with duplicate: false.
Use a stable id from your system, such as a ticket id. It’s safe to retry after a timeout.
On an anonymous survey, the response is stored without a person or company.
Link a response to a record
A response can be about one record: a deal, a ticket, a Company’s plan or a record in a custom data model. Send asubject and the response shows up on that record, and you can filter responses by it.
subject takes one of these shapes:
You can send
subject in three places:
- the body of
POST /surveys/{id}/responses - the body of
POST /surveys/{id}/session, for an in-app survey - the
subjectclaim of a signed in-app token (see In-app survey SDK)
externalId matches more than one ticket, add source or use the GainTrace id. Each failure returns 422 with a message that says which check failed and how to fix it.
A response to an anonymous survey doesn’t keep its subject. Responses carry subject as { "type", "id" }, or null, on the API and in webhooks. Filter GET /survey-responses with subjectType and subjectId. subjectId needs subjectType.
Invitations and approval
POST /surveys/{id}/invitations requests email invitations. Send exactly one of these in the body:
A segment or audience can match up to 1,000 people in one request. A larger audience returns
400. Narrow the rules or split it into segments.
What happens next depends on the workspace’s invitation review setting, which admins change in Sending and identity in Surveys. You can’t set it through the API.
- Send (the default). Invitations go out at once, or at the survey’s next allowed hour if it has a
sendWindow. - Approve. Each invitation joins the Triage approval queue. A teammate approves it, and only then does the email go out. Approvals expire after 14 days.
reminders, people who haven’t answered get a reminder after afterDays days, up to max times. People who answered are never reminded.
The survey must be active, and every person must be active with an email address and a company. If any person in personIds fails that check, the whole request returns 400 and nothing is queued.
People are skipped, not failed, in three cases:
- An invitation for the same email is already waiting, scheduled or just released.
- The person is inside the survey’s
cooldownDays. That counts the last survey of the same measure they got: an email sent, an invited answer or an in-app view. - The person was surveyed within the workspace frequency rule.
202 with counts:
A
segmentId or audience request adds matched, the number of people the rules found.
To preview a list before anything is queued, use the gt_survey_request_invitations tool in the MCP server. It defaults to a dry run that reports who would be invited and why anyone is skipped.
Target people with segments
A segment is a saved set of Company and People rules. Use one to invite the same group again, or to choose who sees an in-app survey.GET /survey-segmentslists the workspace’s segments.POST /survey-segmentssaves one and returns201. Send aname(unique in the workspace, ignoring case) andrules, an object with acompanygroup and apersongroup. Each group has alogicofandororand up to 20 rules. At least one rule is required.DELETE /survey-segments/{id}archives a segment and returns204. Surveys that still point at it stop matching anyone.
read:surveys and writing needs write:surveys, both with a secret key.
A rule is { field, operator, value }. People rules can use email, name, role, contactRole, isChampion, lastSeenAt, engagementScore, createdAt, or traits.<name> for a trait on the person. Any other People field returns 422. Company rules use Company data-model fields.
Frequency and follow-up rules
Two workspace-wide rules keep people from being over-surveyed and make sure feedback gets an owner.Frequency rule
GET and PUT /surveys/frequency-rule read and replace it.
recontactDays and dismissedRecontactDays accept 0 to 365, and 0 turns the rule off. In companyLimit, people is 1 to 1,000 and days is 1 to 365. People are matched by email address, so a person counts once however often they are asked, and reminders don’t count. Leave companyLimit out of a PUT to keep the saved limit. Send null to turn it off. An invitation skipped by the limit counts in skipped, and an in-app session for a person it blocks returns eligible: false with the reason company_limit. For in-app surveys, a person’s recent surveys and dismissals count across every Company they belong to. Each survey also has its own cooldownDays (0 to 730). It sets the minimum days since the person last got a survey of the same measure (an email sent, an invited answer or an in-app view) before this survey can invite them or show to them in-app. It doesn’t block responses you record through the API.
Response rules
GET and PUT /surveys/case-rules read and replace how new responses are handled.
Both
PUT calls need every field, except companyLimit on the frequency rule. Saving one rule doesn’t change the other.
Webhooks
Subscribe a webhook endpoint to survey events instead of polling. The payloads and signature check are on Webhooks.
A
survey_response.created event from an email or in-app survey can carry only the score. The comment and other answers arrive in a later survey_response.updated.
survey.launched and survey.closed carry id, name, measure, status, previousStatus and changedAt. A status call that doesn’t change the status fires nothing.
Read results
GET /survey-responseslists responses from every native survey, newest first. Filter bysurveyId,companyId,measure,band,status(new,in_progress,resolved,openmeaning not resolved, oroverduemeaning open past your hours-to-close rule),theme,subjectTypewithsubjectId, and afromandtowindow on submission time. Filters combine, and an unknown parameter returns422.themelimits the list to responses whose written feedback was tagged with one of 14 themes:onboarding,reliability,performance,usability,missing_functionality,integrations,support_quality,documentation,pricing_value,billing_admin,security_compliance,account_management,reporting_analyticsormobile.GET /survey-responses/{id}reads one response.GET /surveysandGET /surveys/{id}read survey definitions and status.
Company scores and coverage
A Company’s NPS, CSAT and effort score count each person once, under one workspace rule:GET /surveys/rollup-rulesandPUT /surveys/rollup-rulesread and set that rule: each person’slatestanswer, or themeanormedianof their answers, insidewindowDays, with optionalroleWeightsfor decision makers, executive sponsors and champions.GET /companies/{id}/survey-scoresreturns one Company’s scores under the rule, how many people each rests on, whether a decision maker answered, and amethodsentence to show beside the numbers.GET /surveys/coveragereturns who you hear from: Companies and ARR with a scored answer in the window, at how many a decision maker answered, and Companies renewing in the next 90 days with no feedback in 180 days. ARR is in the workspace reporting currency; currencies with no exchange rate yet are listed inunconvertedCurrenciesand left out.
survey_response webhook payload: score, scaleMax, band, comment, answers, companyId, personId, subject, status and submittedAt. Reviewer notes never leave GainTrace.
Survey templates
UseGET /survey-templates to list the templates your workspace saved, each with its full survey definition (scope read:surveys). Use POST /survey-templates to save one (scope write:surveys). Send name, an optional folder, and either surveyId to save an existing survey or input with a survey definition. It returns 201 with the template. Both need a secret server key: a gt_pub_ key returns 403. A name another template uses returns 422.
Logic, jumps and endings
A question can carrylogic, jumps and libraryId, and settings.endings holds named thank-you screens.
logicis{ "match": "all" | "any", "conditions": [...] }with 1 to 10 conditions. A condition is either an earlier answer,{ "question", "operator", "value" }, or a hidden field,{ "hidden", "operator": "is" | "is_not", "value" }. Answer operators arelte,gte,eq,is,is_not,includes,excludes,before,afterandband. Don’t sendlogicandshowWhentogether.jumpsis up to 10 rules, each{ "when": <logic group>, "to": { "question": n } | { "ending": id } | { "end": true } }. The first matching rule wins.settings.endingsis up to 10 entries withid,name, and optionaltitle,messageandlink.libraryIdlinks a question to the workspace question library.
Languages
A survey is written in adefaultLanguage and can offer translations. Both live in settings, so you set them with POST /surveys and PATCH /surveys/{id}. You can change them after launch.
settings.defaultLanguageis one of 44 language codes, such asen,fr,de,pt-BR,zh-TWorar. Without it, the default is English.settings.translationsis an object keyed by language code, with up to 15 entries. The default language’s entry holds onlyuioverrides.
title, a questions array, a thankYou object, an endings object keyed by ending id (each with title, message and linkLabel) and ui overrides for the buttons and labels around the survey.
questions, the entry at each position translates the question at the same position. Use null to leave a question in the default language. An entry can translate text, description, linkLabel, options, rows and scaleLabels.
GainTrace stamps each question translation with a source fingerprint of the default copy when you save. A translation shows only while the default copy is unchanged. If you edit a question’s default copy, respondents see the default copy until you update the translation.
Respondents see the language picked from, in order:
- The language the request asks for: the
?lang=parameter on the survey link, such as/survey/{id}?lang=fr, orlangin an in-app session request. - The person’s
localetrait, then theirlanguagetrait, on personal links, in-app surveys and email. - The browser’s language.
- The survey’s
defaultLanguage.
fr-CA shows fr. The survey link page sets the page language, and lays out right to left for Arabic, Hebrew, Persian and Urdu. Choice answers are stored against the default-language options, so reports stay comparable. Each response records the language it was answered in. The API returns it as language, and warehouse exports carry it as language.
The AI follow-up label and its disclosure aren’t translatable. They stay in English. In-app surveys and invitation emails use the same translations. See the in-app SDK guide for lang.
AI follow-up question
settings.aiProbe is { "enabled": true } or { "enabled": false }, and it is off unless you turn it on. When it is on, Trace AI can ask one optional follow-up question after a respondent’s feedback is saved. Respondents always see that the question was written by AI.
The question and the answer come back inside the response as answers._probe, with question and answer (null until answered). It appears only when a follow-up was asked. survey_response.updated fires when the respondent answers it.
Import from other survey tools
GainTrace can pull answers from Typeform, SurveyMonkey, Qualtrics and Retently, and import CSV exports. Connect a tool in the app, under Surveys, Data sources. The API reads those connections and lets you pull one on demand. It doesn’t create connections or return credentials.GET /survey-importslists connected sources. Each hasid,provider,name,externalSurveyId,measure,scaleMin,scaleMax,status(active,pausedorerror),credentialHint,connectedAt,lastRunAt,lastSuccessAt,lastError,nextRunAtandlastRun(imported,updatedandskippedcounts, ornull). It needsread:surveys.POST /survey-imports/{id}/runspulls one source now, for up to 60 seconds, and needswrite:surveys. It returns404for an unknown id, and409while another pull is running or when the source’s credential was rejected.
status is succeeded, partial (the pull stopped early and continues on the next pull) or failed. skipped is the total. skip_reasons lists up to 20 reasons with counts, and never includes email addresses.
Imported answers are matched to People by email and keep their original dates. Pulls repeat safely, because each answer is identified by its provider and response id. History from before you connected arrives quietly, with no webhooks, Flows or alerts. Answers dated after you connected fire survey_response.created and survey_response.updated with the channel import.
Imported NPS appears in GET /nps-responses (filter by provider). Imported CSAT and CES appear in survey reporting and Company scores, but not in GET /survey-responses.
Bulk export to your warehouse
For analytics in BigQuery or Snowflake, Enterprise workspaces can export every survey and response to tables in their own warehouse. See Survey warehouse export for the table schema.Next steps
In-app survey SDK
Show a survey inside your product.
Webhooks
Get survey events as they happen.
Survey warehouse export
Query responses in BigQuery or Snowflake.