Skip to main content
The in-app SDK shows a survey inside your product, on top of the page your customer is already using. GainTrace decides whether the signed-in person should see it, and your page displays it. You can install it in two ways. Both end in the same survey, with the same rules about who sees it and how often. Your API key never reaches the browser in either method.

Before you start

  • A survey that is active, with in-app collection turned on and at least one allowed origin. You can set this in the survey’s in-app settings, or with settings.inApp on the Surveys API.
  • The GainTrace person who is signed in to your product. A signed token matches them by its sub claim. A backend session uses the contactId and accountId you send.

Allowed origins

settings.inApp.allowedOrigins lists the exact origins that may show the survey, such as https://app.example.com. No paths, no trailing slash, no wildcards. Use HTTPS. For development, http://localhost and http://127.0.0.1 are also accepted. You can list up to 20.

Install with a signed token

Your server signs a token for the signed-in person. Your page passes it to the SDK, and GainTrace checks the signature and decides whether to show the survey. Your backend makes no call to GainTrace per session.

Step 1: create an identity key

In Surveys, open Sending and identity and create an identity key. You need the permission to manage API keys, which admins have by default.
  • GainTrace shows the secret once, when you create the key. Store it as a server secret.
  • The key has an id that starts with gtk_. You put it in the token header as kid.
  • A workspace can have 2 active keys at a time. Revoked keys stay listed for 90 days.
  • Revoking a key stops tokens signed with it from verifying at once.
To rotate a key, create the second key, deploy it to your server, then revoke the first. Until a workspace has an active key, client-side identify is off and the endpoint refuses requests with 403.

Step 2: sign a token on your server

Sign a JWT with HS256, using the key’s secret as the HMAC secret and the key id as kid. GainTrace accepts only HS256. Tokens with any other algorithm are refused. A token can be at most 4,096 characters.
Sign a new token each time the page loads. An expired token returns 401.

Step 3: identify and show

Load the script, hand the token to the SDK, and ask for the survey.
The SDK keeps the token in memory only. It never writes it to storage. Call GaintraceSurveys.reset() when the person signs out. It forgets the token, stops any pending checks and removes an open survey without recording a dismissal. show({ survey }) returns a promise. It resolves to the open survey, or to null when the person doesn’t qualify yet. If the survey is waiting on a page rule or a trigger, the SDK keeps checking, as described in Targeting. The delay comes from the survey’s settings (delayMs). The delay option doesn’t apply to this install.

What the browser sends

The SDK calls POST /api/survey/{id}/identify on the address the script was loaded from. It is a browser endpoint: it needs an Origin header and only answers origins on the survey’s allowed list. It allows 120 requests a minute for each network address. A request that works returns 200. If the person qualifies you get eligible: true and an embedUrl. If not, you get eligible: false and a reason (see Who sees the survey). The reason unknown_person means the token’s sub, email and company didn’t match anyone in your workspace. show rejects with an Error that carries the status as status.

Install with a backend session

Your server asks GainTrace for a session for the signed-in person. Your page loads the SDK and shows the URL the session returned.

Step 1: get a session from your backend

Create a route in your application that makes this request with the signed-in person’s ids, then returns the data object to the browser. You need a secret server key (gt_live_) with the write:surveys scope.
parentOrigin is the exact origin of the page that will show the survey. It must be on the allowed list. contactId and accountId are the GainTrace ids of the person and their Company. context is optional. Send it when the survey has page rules or a trigger. url is the page the person is on, without the # fragment. It must be on parentOrigin, or the request returns 422. You can also send event and dwellSeconds, described in Targeting. The body can carry a subject too, to tie the answer to a record.
Call this endpoint from your server only. A request that carries an Origin header is refused with 403, and cross-origin preflight requests are refused too. Don’t ship the key to the browser.

The response

When the person qualifies, you get a signed URL:
The embedUrl is valid for one hour. Request a session when you intend to show the survey, not ahead of time. When the person doesn’t qualify, you get eligible: false and a reason. See Who sees the survey.

Step 2: add the script to your page

Load the script, ask your backend for the session, and show the survey when it’s eligible.
In a single-page app, call GaintraceSurveys.show after sign-in. Don’t call GaintraceSurveys.destroy() on sign-out: it acts like Close survey, so a survey that has loaded and not been answered counts as a dismissal. A dismissal suppresses in-app surveys for that person for the workspace’s dismissedRecontactDays.

Options

GaintraceSurveys.show(options) takes one object.

Placement

Set the default in settings.inApp.position, or override it per call with the position option. On a narrow screen every position shrinks to the window width minus 32px. A banner or corner card lets people answer when it suits them. Use center when you need an answer before they continue.

Return value

show() gives you the open survey: an object with url, destroy() and snooze(). With a backend session it returns that object directly. With a signed token it returns a promise that resolves to that object, or to null.
  • Calling show() again with the same URL while a survey is open returns the open one.
  • Calling it with a different URL closes the open survey and starts the new one.
  • GaintraceSurveys.destroy() closes the open survey, if any.
  • destroy(), and show() with a different URL, act like Close survey. Once the survey has loaded and the person hasn’t answered, it counts as a dismissal. In test mode it doesn’t.
show() throws an error with a message when something is wrong. For a backend session, the URL must be HTTPS (or localhost), must be the embed URL your backend returned, and the session must match this page’s origin and the display mode. The page’s origin must equal the parentOrigin you sent. If it doesn’t, move the script to an allowed origin or send the right parentOrigin.

Snooze and dismiss

The survey header has two buttons.
  • Close survey counts as a dismissal. The person won’t see an in-app survey again until the dismissedRecontactDays rest period ends, 7 days by default.
  • Ask me later hides the survey, and it can show again after 24 hours. The snooze applies to that survey and that person. After someone answers, the button goes away.
The open survey object does the same from code: destroy() is Close survey and snooze() is Ask me later. Both only record something once the survey has loaded and the person hasn’t answered. In test mode they just remove the survey. Escape closes the survey the same way as Close survey.

Targeting

Targeting decides who sees the survey, on which pages, and when. Set it on the survey in settings.inApp.targeting, or in the survey’s in-app settings under Targeting. It has three parts, and all of them are optional.
PATCH /surveys/{id} replaces the whole inApp object when you send it, so send every inApp field, not just targeting.

Who

audience is either a saved segment, { "segmentId": "..." }, or rules, { "rules": { "company": {...}, "person": {...} } }. Both rule groups are required. Each group has a logic of and or or and up to 20 rules. A rule is { field, operator, value }. An empty group matches everyone. People rules can use these fields: email, name, role, contactRole, isChampion, lastSeenAt, engagementScore and createdAt. They can also use traits.<name> for a trait on the person. Any other People field returns 422. Company rules use Company data-model fields.
Archived segments match nobody. Save segments with the survey segments endpoints. A person who isn’t in the audience gets not_targeted.

Where

pages limits the survey to pages that match.
  • include lists the pages that can show it. A page needs to match any one of them. With no include rules, every page can show it.
  • exclude lists pages that never show it. An excluded page wins over an included one.
  • Each rule has a match of equals, contains or starts_with and a value. Values are matched against the page’s path and query string, such as /billing or /reports?view=weekly. They can’t contain spaces or #, and are at most 512 characters. Each list holds up to 20 rules.
  • Matching is case-sensitive. It ignores the fragment and a trailing slash.
  • equals with a value that has no ? compares only the path, so /reports matches /reports?view=weekly. Add the query to the value to match it exactly.
On a page that doesn’t match, the answer is page_not_targeted.

When

trigger decides the moment. Event names start with a letter or number and then use letters, numbers, dots, dashes, colons and underscores, up to 64 characters.
Until the trigger fires, GainTrace answers eligible: false with reason: "awaiting_trigger" and an awaiting object that lists the events or the timeOnPageSeconds it waits for.

How the SDK waits

With the signed token install, the SDK does the waiting for you, in memory only. Nothing is stored.
  • When the answer is page_not_targeted, it asks again whenever the URL’s path or query changes, including single-page app navigation.
  • When the answer is awaiting_trigger for time on the page, it counts the seconds the tab is visible on the current URL and asks again when the count reaches the threshold. The count restarts when the path or query changes. A change to only the # fragment doesn’t restart it.
  • GaintraceSurveys.track(name) only does something for a survey that is already waiting for that exact event. Call show first, then track.
  • Each check is a request to the identify endpoint. The SDK never throws from these background checks. If GainTrace answers 401, 403, 404 or 422, it stops checking that survey.
  • Reloading the page starts again.
With the backend session install, the SDK doesn’t wait. Your backend sends context.url for page rules, and context.event or context.dwellSeconds to satisfy a trigger, each time it requests a session. Test mode (mode: "test") ignores targeting, so you can check the install on any page.

Order of checks

GainTrace checks that the survey is active, then the origin, the cooldown and recontact rules, dismissals, sampling and snooze. Then it checks targeting: page, audience, trigger. Only after targeting passes does it check already_seen, minimum_age, minimum_sessions and display_limit. A visit counts toward minSessions only after targeting passes.

How the survey behaves

  • Focus. The centered modal traps focus and returns it to the element that had it before.
  • Reduced motion. The survey slides up over 200ms. If the person prefers reduced motion, it fades in instead.
  • Size. The survey resizes itself to fit its content, between 100px and 4,000px tall. Buttons are at least 44px tall.
  • Test mode. With mode: "test" you see a preview. Nothing is recorded, and closing it just removes it.
A survey that hasn’t finished loading when someone closes it is removed without recording a dismissal.

Language

In-app surveys show the survey in the language you ask for, using the translations on the survey. GainTrace picks the language in this order:
  1. The language you pass. With a signed token, pass lang to GaintraceSurveys.show({ survey, lang }). With a backend session, send lang in the session request. It wins over everything else.
  2. The person’s locale trait, then their language trait.
  3. The browser’s language.
  4. The survey’s default language.
lang must be a language code such as es or pt-BR, or show throws “Pass a language code such as es or pt-BR.” The widget’s own buttons (Ask me later and Close survey) and its screen reader announcement switch to that language too, and right-to-left languages (Arabic, Hebrew, Persian and Urdu) mirror the widget and the survey.

Who sees the survey

GainTrace applies these rules when a session is requested. A person who fails one gets eligible: false with the reason below, and your page shows nothing. A person sees each survey at most once per UTC day. Sessions count one per UTC day. In test mode these rules are skipped, but the survey must still be active, in-app collection must be on, and the origin must be allowed. The frequency rule is set for the whole workspace. See Surveys API.

Security and CSP

  • The survey runs in an iframe from the GainTrace address. GainTrace only lets the origin you named in the session frame it. The iframe is sandboxed with allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox and sends no referrer. Popups are allowed so a thank-you button can open a link in a new tab. The survey can’t navigate your page.
  • The script only accepts messages from that iframe.
  • If your site sets a Content Security Policy, allow the GainTrace origin in script-src for surveys.js and in frame-src for the survey. For the signed token install, also allow it in connect-src, because the page calls the identify endpoint.
  • The signed token install uses the address you load surveys.js from for the identify call and for the survey frame. The backend session returns embed URLs on your custom survey domain while the white-label add-on is active, so with that install list the custom domain in frame-src as well.
  • The script adds a style element and inline styles to your page. If your style-src blocks inline styles, the survey won’t render correctly.
  • The script doesn’t write to your page’s storage. The signed token stays in memory.

Troubleshooting

Last modified on October 20, 2026