# Attributes Source: https://docs.usertour.io/api-reference-v2/attributes Custom attributes on users, companies, and events — naming and data types. Users, companies, and events carry custom **attributes** that extend the default data model. The schema of your attributes is exposed by the **attribute definitions** endpoint (`attribute:read`); the values live on each user/company object. ## Naming A `codeName` created through the API must start with a letter and contain only letters, digits, and underscores, 2–100 characters (`snake_case` is the convention — `signed_up_at`, `last_login_time`). It is case-sensitive and unique per scope. SDK ingestion is deliberately more lenient: an attribute key your app sends that isn't defined yet is **auto-created** with a type inferred from the first value — so send a number as `42` (not `"42"`) and dates as full ISO timestamps; a wrongly inferred type can be corrected later while no stored value conflicts. Display names (shown in the Usertour UI) are configured separately — e.g. `signed_up_at` can display as "Signed Up". ## Data types | Type | Description | | ---------- | --------------------------------- | | `string` | A string. | | `number` | Integer or floating point. | | `boolean` | `true` or `false`. | | `datetime` | A point in time, ISO 8601 in UTC. | | `list` | A list of strings. | ## Best practices * Use the most specific type — store dates as `datetime`, flags as `boolean` (not strings). * Keep names concise but descriptive; avoid system/reserved names. * Don't store large blobs in attributes. # Authentication Source: https://docs.usertour.io/api-reference-v2/authentication Personal API tokens, scopes, and project access. The v2 API authenticates with **personal API tokens**. Create one in your Usertour dashboard under **Settings → Personal API keys**, then send it as a Bearer header on every request: ``` Authorization: Bearer utp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` A token is owned by a user and has three properties that gate every request: * **Projects** — the token is bound to one or more projects. A request to `/v2/projects/{projectId}/...` is rejected unless the token includes that project. * **Scopes** — fine-grained capabilities. A request is rejected unless the token carries the scope its endpoint requires. * **Environments** — an optional allowlist. Environment-targeted operations (publishing, end-user data, sessions, …) are rejected outside it (`E1029`); with no allowlist the token covers every environment of its projects. **The environment allowlist is not a content-isolation boundary.** It fences *delivery and end-user data* — publishing / unpublishing, users, companies, sessions, segment membership, analytics, and the environment records themselves (an out-of-scope environment's SDK token is withheld). It does **not** limit which content a token can see or change: content, versions and themes are **project-level**, so any token carrying content scopes can read every piece and every version in the project — including one that is live in an environment it may not act on — and can edit or delete them with `content:update` / `content:delete`. So a "staging-only" token still reads (and can rewrite) what is live in production. If you need someone genuinely walled off from another environment's content — a contractor, an agency — put that work in a **separate project**. Connections authorized over [MCP OAuth](/api-reference-v2/mcp) hold `uto_` tokens with the same three properties (granted on the consent screen); they authenticate REST requests exactly like a personal `utp_` token. ## Scopes | Scope | Grants | | -------------------- | --------------------------------------------------------- | | `content:read` | Read content and versions | | `content:create` | Create / duplicate content, create draft versions | | `content:update` | Edit drafts (steps, rules, theme, body), restore versions | | `content:delete` | Delete content | | `content:publish` | Publish / unpublish a version to an environment | | `theme:read` | List / get themes | | `theme:create` | Create themes | | `theme:update` | Update themes | | `theme:delete` | Delete themes | | `user:read` | Read end-users (and their event history) | | `user:write` | Create or update end-users | | `user:delete` | Delete end-users | | `company:read` | Read companies (and their event history) | | `company:write` | Create / update companies and manage memberships | | `company:delete` | Delete companies | | `session:read` | Read content sessions | | `session:manage` | End or delete content sessions | | `attribute:read` | Read attribute definitions | | `attribute:create` | Create attribute definitions | | `attribute:update` | Update attribute definitions | | `attribute:delete` | Delete attribute definitions | | `event:read` | Read event definitions | | `event:create` | Create event definitions | | `event:update` | Update event definitions | | `event:delete` | Delete event definitions | | `segment:read` | List / get segments | | `segment:create` | Create segments | | `segment:update` | Update segments and manage manual members | | `segment:delete` | Delete segments | | `analytics:read` | Read analytics data | | `environment:read` | List / get environments | | `environment:manage` | Create / rename / delete environments | Grant a token only the scopes it needs. ## Errors | Situation | Status | Code | | ------------------------------- | ------ | ------- | | Missing `Authorization` header | 401 | `E1010` | | Unknown / invalid token | 403 | `E1000` | | Token not scoped to the project | 403 | `E1011` | | Token lacks the required scope | 403 | `E1012` | See [Errors](/api-reference-v2/errors) for the full list. ## Using a token with MCP The [MCP endpoint](/api-reference-v2/mcp) carries no project in its path, so a token used with MCP must be scoped to **exactly one project**. Multi-project tokens work for the REST API but are rejected by MCP tool calls. # Blocks Source: https://docs.usertour.io/api-reference-v2/blocks The visual building units of content — text, image, button, embed, question, and columns — with their exact payloads. **Blocks** are the visual units that make up content. They appear in a flow step's `content[]`, and in the rich content of checklists and banners. (The resource center has its own block vocabulary — see [Type-specific data](/api-reference-v2/type-data#resource-center).) A block is a small tagged object: ```json theme={null} { "object": "block", "id": "bk_…", "type": "text", "markdown": "Hello" } ``` * **`id`** is the field-merge write handle. Echo a block's `id` to update it in place (styling and other unmodeled details are preserved); **omit** `id` to create a new block. (`object: "block"` is returned on read; you don't send it.) * **`type`** selects the block shape below. Rich text is a small **markdown subset** — see [Content representation](/api-reference-v2/content-representation#rich-text). Conditions and actions referenced here are documented in [Conditions & actions](/api-reference-v2/conditions-and-actions). ## text A rich-text paragraph block. | Field | Type | Required | Description | | ---------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | | Field-merge write handle. | | `type` | `"text"` | yes | | | `markdown` | string | yes | A small markdown subset: paragraphs, `# `/`## ` headings (h1/h2 only — no h3+), `-`/`*` and `1.` lists, \`\`\` code fences; inline `**bold**`, `*italic*`, `[text](url)`, and `{{ attribute_code \| default: "x" }}` for user attributes. Anything outside this subset is SILENTLY normalized, not rejected: h3+ → h2; blockquotes flatten to paragraphs; tables, horizontal rules, strikethrough, inline images/code, and liquid filters other than `default` are dropped. Unsupported syntax won't round-trip — don't rely on it. | ```json theme={null} { "type": "text", "markdown": "Welcome **{{ first_name | default: \"there\" }}** 👋" } ``` ## image | Field | Type | Required | Description | | -------- | --------------------------- | -------- | ------------------------- | | `id` | string | | Field-merge write handle. | | `type` | `"image"` | yes | | | `url` | string | yes | Image URL (http/https). | | `alt` | string | | Alt text. | | `link` | `object{ url, newTab }` | | Wrap the image in a link. | | `width` | [Dimension](#shared-shapes) | | Image width. | | `margin` | [Spacing](#shared-shapes) | | Outer margin. | ```json theme={null} { "type": "image", "url": "https://example.com/logo.png", "alt": "Logo", "width": { "unit": "percent", "value": 80 }, "link": { "url": "https://example.com", "newTab": true } } ``` ## button | Field | Type | Required | Description | | -------------- | ------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | | Field-merge write handle. | | `type` | `"button"` | yes | | | `text` | string | yes | Button label. | | `actions` | [Action](/api-reference-v2/conditions-and-actions#actions)\[] | | What clicking does (e.g. go to a step, dismiss). | | `disabledWhen` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | REACTIVE slot — polled live in the browser (the button disables the moment the conditions match). Client-evaluable condition types only: attribute / current\_url / element / text\_input / text\_filled / time\_window; event / segment / content\_state are rejected (E1017). | | `hiddenWhen` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | REACTIVE slot — polled live in the browser (the button shows/hides as conditions change). Same client-evaluable-only rule as `disabledWhen`. | | `variant` | `primary` \| `secondary` | | Visual style. | | `margin` | [Spacing](#shared-shapes) | | Outer margin. | A button needs both `text` **and** at least one action to be publishable. ```json theme={null} { "type": "button", "text": "Next", "variant": "primary", "actions": [{ "type": "goto_step", "step": "pricing" }] } ``` ## embed An embedded URL (video, iframe-able page, …). | Field | Type | Required | Description | | -------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | | Field-merge write handle. | | `type` | `"embed"` | yes | | | `url` | string | yes | Paste the page's normal URL (e.g. a youtube.com/watch link) — on write it is resolved through the standard oEmbed provider registry (YouTube, Vimeo, Loom, Figma, …) and the provider's official embed markup is stored, so you never hand-build /embed/ URLs. A URL no provider claims is iframed as-is — it renders only if that site allows being framed (no X-Frame-Options/CSP block). | | `width` | [Dimension](#shared-shapes) | | Width. | | `height` | [Dimension](#shared-shapes) | | Embed height. Optional ONLY for provider embeds (YouTube/Vimeo/… size themselves by aspect ratio); a URL with NO oEmbed provider has no ratio, and omitting a pixel height leaves the iframe at the browser's built-in default — a strip \~150px tall, almost never the intended size (validate warns). For plain-iframe URLs always set `{ "unit": "pixels", "value": … }`. | | `margin` | [Spacing](#shared-shapes) | | Outer margin. | ```json theme={null} { "type": "embed", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "width": { "unit": "percent", "value": 100 } } ``` ## question A survey question. The `question` field is one of the four [Question](#questions) shapes below; `actions` run after the user answers. | Field | Type | Required | Description | | ---------- | ------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | | Field-merge write handle. | | `type` | `"question"` | yes | | | `question` | [Question](#questions) | yes | The question definition. | | `actions` | [Action](/api-reference-v2/conditions-and-actions#actions)\[] | | Actions that fire when this question is answered (on pick for nps/rating/single-select; on its Submit button for text/multi-select). Put a `goto_step` HERE to advance to the next step — without it the question records the answer but the flow does NOT advance (validate flags the next step "not reachable"). Do NOT add a separate `button` block just to advance: it doubles up with the question's own submit affordance. | ```json theme={null} { "type": "question", "question": { "kind": "rating", "name": "Satisfaction", "style": "star", "range": { "low": 1, "high": 5 } } } ``` ### Questions Every question needs a `name` (used for analytics). `bindAttribute` (optional) stores the answer on a user attribute. `cvid` is server-owned (returned on read). #### `nps` | Field | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"nps"` | yes | | | `name` | string | yes | The question's internal name / analytics label (it is the `questionName` on captured responses). It is NOT rendered to the user — the widget shows only the input (scale / options / text field), not this string. To show a visible question prompt, add a `text` block in the SAME step before the question block; a question with only a `name` renders as bare options with no question text. | | `cvid` | string | | | | `lowLabel` | string | | | | `highLabel` | string | | | | `bindAttribute` | string | | Optional: codeName of an EXISTING attribute (create the attribute definition first) to ALSO save this answer onto the user for targeting/segmentation — use the codeName, NOT the id. The write does not check it, but the version validation WARNS when the attribute is missing or its dataType mismatches the answer — read warnings. A wrong code that slips through silently captures nothing at runtime. Match the attribute dataType to the answer: number (nps / rating), string (single-select choice), list (multi-select choice). Leaving it unset still records the answer as a response event — bind only when you need to target/segment on it. | #### `rating` | Field | Type | Required | Description | | --------------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"rating"` | yes | | | `name` | string | yes | The question's internal name / analytics label (it is the `questionName` on captured responses). It is NOT rendered to the user — the widget shows only the input (scale / options / text field), not this string. To show a visible question prompt, add a `text` block in the SAME step before the question block; a question with only a `name` renders as bare options with no question text. | | `cvid` | string | | | | `style` | `star` \| `scale` | yes | star = star rating; scale = a numeric scale. A "scale" question IS a rating with style:"scale" — there is no separate "scale" kind. | | `range` | `object{ low, high }` | yes | Numeric range, e.g. `{ low: 1, high: 5 }`. | | `lowLabel` | string | | | | `highLabel` | string | | | | `bindAttribute` | string | | Optional: codeName of an EXISTING attribute (create the attribute definition first) to ALSO save this answer onto the user for targeting/segmentation — use the codeName, NOT the id. The write does not check it, but the version validation WARNS when the attribute is missing or its dataType mismatches the answer — read warnings. A wrong code that slips through silently captures nothing at runtime. Match the attribute dataType to the answer: number (nps / rating), string (single-select choice), list (multi-select choice). Leaving it unset still records the answer as a response event — bind only when you need to target/segment on it. | #### `text` | Field | Type | Required | Description | | --------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"text"` | yes | | | `name` | string | yes | The question's internal name / analytics label (it is the `questionName` on captured responses). It is NOT rendered to the user — the widget shows only the input (scale / options / text field), not this string. To show a visible question prompt, add a `text` block in the SAME step before the question block; a question with only a `name` renders as bare options with no question text. | | `cvid` | string | | | | `multiline` | boolean | yes | | | `placeholder` | string | | | | `buttonText` | string | | | | `required` | boolean | | Require an answer before submit. ONLY `text` supports this — nps / rating / choice cannot be marked required. | | `bindAttribute` | string | | Optional: codeName of an EXISTING attribute (create the attribute definition first) to ALSO save this answer onto the user for targeting/segmentation — use the codeName, NOT the id. The write does not check it, but the version validation WARNS when the attribute is missing or its dataType mismatches the answer — read warnings. A wrong code that slips through silently captures nothing at runtime. Match the attribute dataType to the answer: number (nps / rating), string (single-select choice), list (multi-select choice). Leaving it unset still records the answer as a response event — bind only when you need to target/segment on it. | #### `choice` | Field | Type | Required | Description | | ------------------ | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `"choice"` | yes | | | `name` | string | yes | The question's internal name / analytics label (it is the `questionName` on captured responses). It is NOT rendered to the user — the widget shows only the input (scale / options / text field), not this string. To show a visible question prompt, add a `text` block in the SAME step before the question block; a question with only a `name` renders as bare options with no question text. | | `cvid` | string | | | | `options` | `object{ label, value }`\[] | yes | Each option has a human-facing `label` and a stored `value` — the `value` is what gets recorded/bound as the answer. | | `allowMultiple` | boolean | yes | false = single-select, true = multi-select. A multi-select answer needs a `list`-typed bound attribute. | | `enableOther` | boolean | | | | `otherPlaceholder` | string | | | | `shuffle` | boolean | | | | `buttonText` | string | | | | `bindAttribute` | string | | Optional: codeName of an EXISTING attribute (create the attribute definition first) to ALSO save this answer onto the user for targeting/segmentation — use the codeName, NOT the id. The write does not check it, but the version validation WARNS when the attribute is missing or its dataType mismatches the answer — read warnings. A wrong code that slips through silently captures nothing at runtime. Match the attribute dataType to the answer: number (nps / rating), string (single-select choice), list (multi-select choice). Leaving it unset still records the answer as a response event — bind only when you need to target/segment on it. | ## columns A row of side-by-side columns, each holding its own blocks — the only nesting in the block model. | Field | Type | Required | Description | | --------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `id` | string | | Field-merge write handle. | | `type` | `"columns"` | yes | | | `columns` | Column\[] | yes | One entry per column, laid out left-to-right. Each column is a mini vertical stack of `blocks` (usually one). | ```json theme={null} { "type": "columns", "columns": [ { "width": { "unit": "percent", "value": 50 }, "blocks": [{ "type": "text", "markdown": "Left" }] }, { "blocks": [{ "type": "image", "url": "https://example.com/x.png" }] } ] } ``` ## Shared shapes Small objects reused across blocks. **Dimension** — a width/height. | Field | Type | Required | Description | | ------- | ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `unit` | `percent` \| `pixels` \| `fill` | yes | `fill` ignores `value`, and is a **column-only** unit — an image/embed width or height with `fill` is rejected. | | `value` | number | | ≥ 0. For `percent`, values over 100 are allowed (overflow). | **Spacing** — margin/padding box (pixels; omitted sides inherit the theme). | Field | Type | Required | Description | | ----------------------------------- | ------- | -------- | ---------------------------------- | | `enabled` | boolean | | Whether spacing is applied. | | `top` / `bottom` / `left` / `right` | number | | Per-side pixels (may be negative). | **Column** — one column inside a `columns` block. | Field | Type | Required | Description | | --------- | ----------------------------------------------------------------- | -------- | ------------------------ | | `width` | Dimension | | Column width. | | `justify` | `start` \| `center` \| `end` \| `between` \| `around` \| `evenly` | | Horizontal distribution. | | `align` | `start` \| `center` \| `end` \| `baseline` | | Vertical alignment. | | `padding` | Spacing | | Inner padding. | | `blocks` | Block\[] | yes | The column's blocks. | # Conditions & actions Source: https://docs.usertour.io/api-reference-v2/conditions-and-actions The reusable predicate and behavior primitives — used by start/hide rules, step triggers, and button/question actions. **Conditions** decide *whether* something applies; **actions** decide *what happens*. They're the same shapes everywhere they appear: version [start/hide rules](/api-reference-v2/rules), step triggers, and button / question `actions`. ## Conditions A condition list is **AND** by default. Use a `group` to express **OR** (or to nest): a group's `match` is how its children combine (`all` = AND, `any` = OR). A group must hold **at least one** condition — an empty group is not "no filter", it is a node that never matches, so next to an AND it makes the whole rule unmatchable. Writing one is rejected. A version saved with an empty group in the builder still reads back with it and draws a validate warning; writing that list back is refused until the group is filled or dropped. ```json theme={null} [ { "type": "segment", "segment": "cm9f6vwed0002iejc4vg2zu3t", "in": true }, { "type": "group", "match": "any", "conditions": [ { "type": "current_url", "includes": ["/pricing"] }, { "type": "current_url", "includes": ["/billing"] } ]} ] ``` #### `group` | Field | Type | Required | Description | | ------------ | ------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"group"` | yes | | | `match` | `all` \| `any` | yes | | | `conditions` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | yes | The grouped conditions — at least one. An EMPTY group is not "no filter": it never matches, so next to an AND it makes the whole rule unmatchable, and writing one is rejected. A version saved with an empty group in the BUILDER still reads back with it (validate warns); writing that list back is refused until the group is filled or dropped. | #### `attribute` | Field | Type | Required | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"attribute"` | yes | | | `scope` | `user` \| `company` \| `companyMembership` | yes | Which entity owns the attribute — `user` (the end user), `company`, or `companyMembership`. Same value as the attribute definition's `scope` (see the attribute definitions list); required to disambiguate a codeName that exists in more than one scope. | | `attribute` | string | yes | | | `op` | `is` \| `not` \| `contains` \| `not_contains` \| `starts_with` \| `ends_with` \| `any` \| `empty` \| `lt` \| `lte` \| `gt` \| `gte` \| `between` \| `true` \| `false` \| `includes_any` \| `includes_all` \| `not_includes_any` \| `not_includes_all` \| `less_than` \| `exactly` \| `more_than` \| `before` \| `on` \| `after` | yes | Operator — the allowed set depends on the attribute dataType. String: is \| not \| contains \| not\_contains \| starts\_with \| ends\_with \| any \| empty. Number: is \| not \| lt \| lte \| gt \| gte \| between \| any \| empty. Boolean: true \| false \| any \| empty. List: includes\_any \| includes\_all \| not\_includes\_any \| not\_includes\_all \| any \| empty. DateTime: less\_than \| exactly \| more\_than (relative — `value` is a number of days) \| before \| on \| after (`value` is an absolute date) \| any \| empty. The relative ops are ONE-SIDED bounds around (now − N days): `less_than N` = the date is AFTER now−N — so it also matches every FUTURE date, and on a future-dated attribute (a trial end, a renewal date) it is NOT "within the last N days"; `more_than N` = the date is BEFORE now−N. **"Signed up in the last N days" therefore needs BOTH bounds** — `less_than N` AND `more_than 0` in one `all` group; `less_than N` alone silently includes anyone whose date is in the future (a mis-mapped trial-end column, a clock/timezone slip), and those are exactly the users a new-user audience must not contain (observed in testing). Negative N shifts the bound into the future: the rolling "within the NEXT 7 days" window is `less_than` value "0" AND `more_than` value "-7" (two conditions, both required). The relative ops are DAY-granularity only — no unit field; for hour/minute windows use an `event` condition with a `within` (which has a `unit`). | | `value` | string | | The comparison value (string / number-as-string / date). Omit for any/empty/true/false. | | `value2` | string | | Upper bound for the `between` operator (`value` is the lower bound). | | `values` | string\[] | | Values for the List operators (includes\_any / includes\_all / …). | The same shape with `type: "event_attribute"` (and no `scope`) filters on an **event's own** attributes — valid only inside an `event` condition's `where`. #### `segment` | Field | Type | Required | Description | | --------- | ----------- | -------- | ----------- | | `type` | `"segment"` | yes | | | `segment` | string | yes | | | `in` | boolean | yes | | #### `current_url` | Field | Type | Required | Description | | ---------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | `"current_url"` | yes | | | `includes` | string\[] | yes | URL patterns (anchored whole-url match, NOT substring/regex). `*` = wildcard within one url part; `:name` = one path segment. A bare `*` (i.e. `["*"]`) matches EVERY page on every host incl. deep paths — the canonical always-on / whole-site pattern (use it when content should be available everywhere). Scope it down instead with: `*/` (homepage only — path exactly `/`), `*/pricing` (one page), `*/app/*` (a section + below), `host.com/*` (any page on a specific host). Multiple patterns are OR-matched: the URL matches this list if it matches ANY one pattern (so "/tasks OR /dashboard" is one condition with both patterns here — no group needed). | | `excludes` | string\[] | | URL patterns to exclude (same syntax as includes); excludes win over includes. | #### `element` | Field | Type | Required | Description | | -------- | ---------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"element"` | yes | | | `target` | Target | | | | `state` | `present` \| `hidden` \| `disabled` \| `enabled` \| `clicked` \| `unclicked` | yes | `present` means NOT CLIPPED AWAY: the element is in the DOM and its box lies inside the viewport / its scroll ancestors — scrolled off-screen or `display:none` never satisfies it (and `hidden` is its negation). It is NOT "the user can see something there": an EMPTY, zero-height placeholder node satisfies `present` (observed in testing: a checklist task keyed on an initially-empty `

` status line ticked itself the moment the checklist appeared, before the shopper did anything). So do not use element presence as a proxy for "the app has said something": most apps keep the container mounted and only fill in its text. Match the TEXT instead (`target.text` + `present`, or the negation trick: the old text `hidden`). Appearances shorter than about a second can be missed entirely. `disabled`/`enabled` read the element disabled state at evaluation time. **`clicked` means "clicked since page load AND the element is STILL in the DOM right now"** — both halves, re-checked every evaluation. The click memory latches (the listener attaches the FIRST time the condition is evaluated, so earlier clicks are invisible, and the memory survives a re-render), but the element lookup is redone each poll. Two consequences, one of them silent: (1) **an element that UNMOUNTS on click can NEVER satisfy it** — the click lands, the element vanishes, the lookup fails from then on and the condition stays false forever with no error (observed in testing: a tracker on a button that clears its own toolbar counted ZERO real clicks); (2) an element that unmounts and REMOUNTS satisfies it again, so a tracker gated on it fires once per remount — not once per page load. Unlike `present`, this lookup does NOT require viewport visibility: scrolling the target off-screen keeps `clicked` true. `unclicked` negates the same pair, so it is also false while the element is absent. To count a COMPLETED action, condition on what the app shows afterwards (a success toast, a state change) rather than `clicked` on the button that starts it. | #### `content_state` | Field | Type | Required | Description | | --------- | ---------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"content_state"` | yes | | | `content` | string | yes | contentId of the FLOW or CHECKLIST whose per-user state to check (an id from the content list). Only flows and checklists record this state — referencing a banner / launcher / resource-center / tracker is rejected at write. | | `state` | `seen` \| `unseen` \| `completed` \| `uncompleted` \| `active` \| `inactive` | yes | The referenced flow/checklist's state for THIS user. seen = started at least once (for a flow, TRUE from the moment it opens; for a checklist, TRUE only once the user EXPANDS the panel — a `initialDisplay: "button"` checklist whose launcher is never clicked stays unseen forever); unseen = never started; active = currently open/running; inactive = NOT currently running (covers both never-started and ran-then-closed); completed = reached a goal/completion step; uncompleted = not completed. To gate piece B until flow A has run AND closed (the usual "show next thing after the welcome flow" sequencing), use `seen` AND `inactive` together — `seen` alone fires while A is still open (B piles on top), and `completed` alone strands users who skip/dismiss A. | #### `event` | Field | Type | Required | Description | | -------- | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | `"event"` | yes | | | `event` | string | yes | | | `count` | `object{ op, n, n2 }` | | How many times the event must have occurred. Omit it for the common case "the event has happened" (treated as at\_least 1). Set `op`/`n` for a threshold (`between` needs `n` and `n2`). `at_least`/`between` require n ≥ 1; use `at_most`/`exactly` with 0 for "never happened". | | `within` | `object{ op, value, value2, unit }` | | Optional time window for the event count. Omit it (or use `any_time`) to count over all time — "the event has ever happened". Any other `op` (`in_the_last` / `more_than` / `between`) requires BOTH `value` and `unit`, and `between` also `value2` — rejected at write otherwise. | | `scope` | `current_user` \| `current_user_in_company` \| `any_user_in_company` | | Whose event activity to count (default `current_user`). `current_user` = only this user's own events. `current_user_in_company` = this user's events, but counted within their currently-associated company context (needs the user associated to a company via `group()` / the company-membership API). `any_user_in_company` = events by ANY user in this user's company — account-level activity (e.g. "anyone on the account has done X"). The two company scopes require the user to be in a company or they never match. | | `where` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | | #### `text_input` | Field | Type | Required | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | | `type` | `"text_input"` | yes | | | `target` | Target | | | | `op` | `is` \| `not` \| `contains` \| `not_contains` \| `starts_with` \| `ends_with` \| `match` \| `unmatch` \| `any` \| `empty` | yes | | | `value` | string | | | #### `text_filled` | Field | Type | Required | Description | | -------- | --------------- | -------- | ----------- | | `type` | `"text_filled"` | yes | | | `target` | Target | | | #### `time_window` | Field | Type | Required | Description | | ------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | `"time_window"` | yes | | | `start` | string | yes | Window start (ISO datetime). REQUIRED — the runtime never matches a window without a start, so an end-only window is rejected at write. For "until X" semantics, set start to any past instant and end to X. | | `end` | string | | Window end (ISO datetime). Omit for an open-ended "from start onwards" window. | #### `task_clicked` Parameterless (`{ "type": "task_clicked" }`). Valid only in a checklist item's `completeWhen` — the item completes when clicked. #### `unsupported` Read-back only: a stored condition the representation cannot express is returned as `{ "type": "unsupported", "note": "…" }` — usually a DEAD condition the runtime never matches (deleted attribute/event, end-only time window); the `note` says which. It cannot be written back: **echoing it is rejected** (E1017) — the placeholder carries no data to preserve. Either remove it from the list you write, an explicit choice that DELETES the stored condition (mind: a never-matching node inside an AND list pins the whole rule to "never fires", so deleting it can bring the remaining conditions to life), or repair the original condition in the Usertour builder first. The same contract applies to segment conditions. ## Actions #### `goto_step` | Field | Type | Required | Description | | ------ | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `type` | `"goto_step"` | yes | | | `step` | string | yes | Target step: a step `key` declared elsewhere in the same write, or an existing step cvid. Resolved server-side to the cvid. | #### `start_content` | Field | Type | Required | Description | | --------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"start_content"` | yes | | | `content` | string | yes | contentId of the flow or checklist to launch (an id from the content list) — a raw content id, NOT a step key (unlike goto\_step). Must reference a flow or checklist (a banner / launcher / resource-center / tracker is rejected at write). The target must be PUBLISHED to actually start at runtime; an unknown/dangling id is rejected at validate. | | `step` | string | | Optional cvid of a step within the launched flow to start at. | #### `navigate` | Field | Type | Required | Description | | -------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"navigate"` | yes | | | `url` | string | yes | Absolute URL, or an app-relative path ("/docs/x") resolved against the origin the user is on — relative paths are the normal choice for in-app navigation. | | `newTab` | boolean | | Open the URL in a new browser tab instead of navigating the current one. | #### `dismiss` | Field | Type | Required | Description | | ------ | ----------- | -------- | ----------- | | `type` | `"dismiss"` | yes | | `run_javascript` is **echo-only**: builder-authored script actions appear on reads, and echoing one back **unchanged** preserves it when you rewrite the surrounding list (action lists are full replacements — leaving it out deletes it). Authoring a new or edited script is rejected (no API/agent-injected JavaScript). ```json theme={null} { "type": "button", "text": "See pricing", "actions": [{ "type": "navigate", "url": "/pricing", "newTab": true }] } ``` ## Target An element reference (used by `element` / `text_input` / `text_filled` conditions, tooltip steps, and banner container). | Field | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `selector` | string | yes | A stable CSS selector for the element in your app. | | `text` | string | | Visible text the targeted element must **equal** (exact match, after trim) — refines the element chosen by `selector`/`nth`; it does not search among multiple matches. | | `nth` | number | | 0-based index of the match when the selector isn't unique, in document order. Range 0–4 (only the first 5 matches are addressable). | ```json theme={null} { "selector": "[data-tour='create']" } ``` ```json theme={null} { "selector": "button.cta", "text": "Save", "nth": 0 } ``` # Content representation Source: https://docs.usertour.io/api-reference-v2/content-representation How a content version is read and written — steps, blocks, rules, and type-specific data. A piece of **content** (flow, checklist, launcher, banner, tracker, resource-center, announcement) has **versions**. The editable head is the *draft* version (`editedVersionId`); publishing makes a version live in an environment. A version's authorable body is exposed as a stable **representation** — the same shape you read and write — rather than Usertour's internal builder model. You **read** it with `expand`, and **write** it with `PATCH content/{contentId}/versions/{id}` (drafts only). ## A version's body | Content type | Body | Read with | Write field | | ------------------------------------------------------------------------------------ | --------- | --------------- | ----------- | | `flow` (tooltip / modal / …) | **steps** | `?expand=steps` | `steps` | | `checklist` / `launcher` / `banner` / `tracker` / `resource-center` / `announcement` | **data** | `?expand=data` | `data` | Both also carry version-level **start / hide rules** (`startRules`, `hideRules`) and a `themeId`. ## Steps (flow) ```json theme={null} { "object": "step", "id": "cm9...", // server-owned; the write handle (see below) "key": "welcome", // your handle, write-only — wire goto_step by it "name": "Welcome", "type": "modal", "placement": { "position": "center" }, "content": [ { "object": "block", "type": "text", "markdown": "Welcome **{{ first_name | default: \"there\" }}**" }, { "object": "block", "type": "button", "text": "Next", "variant": "primary", "actions": [{ "type": "goto_step", "step": "pricing" }] } ], "triggers": [ { "when": [ /* conditions */ ], "do": [ /* actions */ ] } ] } ``` **Blocks** (`content[]`): `text` (markdown), `image`, `button`, `embed`, `question` (nps / rating / text / choice), and `columns` (a row of nested blocks). Full payload per block type: **[Blocks](/api-reference-v2/blocks)**. **Rich text** is a small markdown subset: paragraphs, `# `/`## ` headings (h1/h2 only — no h3+), `-`/`*` and `1.` lists, ` ``` ` code fences; inline `**bold**`, `*italic*`, `[text](url)` (append `{target=_blank}` right after a link to open it in a new tab), and a Liquid-style user-attribute placeholder `{{ attribute | default: "fallback" }}`. Emphasis applies to placeholders too: `**Hi {{ name }}!**` renders the whole greeting — the interpolated value included — in bold. ### Step identifiers: `id`, `cvid`, `key` * **`id`** — server-owned **write handle**. Echo a step's `id` to update it; **omit** `id` to create a new step. Steps you omit from the list are deleted. * **`cvid`** — server-owned, version-stable reference. Returned on read; survives version copies. You never set one. * **`key`** — **your** handle, **write-only** (not stored, not returned). Give a step a `key` and a `goto_step` action elsewhere in the *same write* can target it with `"step": ""` — so you can author a whole flow, including forward and cyclic links, in one request without knowing cvids yet. A `goto_step` `step` resolves to a `key` you sent in this request, or an existing `cvid` — both map to the same step, so you don't have to choose. The stored value is always the cvid (reads return cvids). ## Rules Conditions and actions are reused everywhere (step triggers, button/question actions, version start/hide rules): * **Conditions** — a recursive tree (`attribute` with a `scope`, `current_url`, `segment`, `element`, `content_state`, `event`, `text_input`, `text_filled`, `time_window`, nested `group` with `all`/`any`). * **Actions** — `goto_step`, `start_content`, `navigate`, `dismiss`. (`run_javascript` is **echo-only**: builder-authored scripts appear on reads and are preserved by echoing them back unchanged; authoring one is rejected.) * **Start rules** (`startRules`) — when the content auto-starts, plus frequency / priority / wait. **Hide rules** (`hideRules`) — when to hide it. Field reference: **[Conditions & actions](/api-reference-v2/conditions-and-actions)** and **[Start, hide & trigger rules](/api-reference-v2/rules)**. ## Type-specific data (non-flow) For the non-flow types, the body lives in `data` (read with `?expand=data`): checklist items, launcher target/tooltip/behavior, banner placement/content, tracker event, or the resource-center block tree. Nested rich content reuses the same blocks, and conditions/actions reuse the same rules. A newly created non-flow content is seeded with its type's default `data`, so you only send the fields you want to change — the write is a field-level merge. Full payload per type: **[Type-specific data](/api-reference-v2/type-data)**. ## Writing `PATCH /v2/projects/{projectId}/content/{contentId}/versions/{id}` — send only the fields you want to change: ```json theme={null} { "steps": [ /* full list, merged by id */ ], "startRules": { "when": [ /* conditions */ ] }, "hideRules": null, // null clears "themeId": "cm7...", // switches theme (cannot be cleared) "data": { /* for non-flow types */ } } ``` * Writes target **draft** versions only. * It is a **field-level merge**: styling, layout, and other details the representation doesn't express are preserved from the existing version. * Omitted top-level fields are left untouched. To start a fresh draft, `POST content/{contentId}/versions` (forks the edited version), or `POST content/{contentId}/versions/{id}/restore` (forks a historical version forward). Publishing is a separate, explicit step — see the **Publish a version** endpoint. # Errors Source: https://docs.usertour.io/api-reference-v2/errors The v2 error envelope and codes. Every v2 error returns a stable JSON envelope with a machine-readable `code` — match on `code`, not on the message (messages may change): ```json theme={null} { "error": { "code": "E1012", "message": "API key lacks the required scope for this operation", "doc_url": "https://docs.usertour.io" } } ``` Validation errors (`E1017`) may additionally carry an `issues` array with one entry **per problem**, so you can fix every field in a single round-trip instead of resubmitting once per error: ```json theme={null} { "error": { "code": "E1017", "message": "steps[0].target: selector is required; steps[1]: unknown step type", "issues": [ { "rule": "schema", "path": "steps[0].target", "message": "selector is required" }, { "rule": "step_shape", "path": "steps[1]", "message": "unknown step type" } ], "doc_url": "https://docs.usertour.io" } } ``` Each issue has a `rule` (which validation layer rejected it: `schema`, `reactive_condition`, `action_not_allowed`, `step_shape`, `reference_target`, `auto_start`, `media_url`), a `message`, and — when it maps to a request field — a `path` into the request body. HTTP status follows the usual conventions: `2xx` success, `4xx` your request, `5xx` Usertour. ## Codes | Code | Status | Meaning | | ------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `E1000` | 403 | Invalid / unknown API token | | `E1010` | 401 | Missing `Authorization` header | | `E1011` | 403 | Token not scoped to the requested project | | `E1012` | 403 | Token lacks the required scope | | `E1019` | 403 | Environment does not belong to the project | | `E1029` | 403 | Environment is outside the token's environment allowlist | | `E1032` | 403 | Creating an environment needs a token WITHOUT env-targeted capabilities (user/company/session/segment/analytics, content:publish) — those must name environments, and no allowlist can cover an environment that does not exist yet; use a separate project-level-only token | | `E0043` | 403 | The project's plan does not include the feature — today: webhook writes below the Starter plan on Usertour Cloud (reads and deletes stay available; self-hosted is never gated) | | `E1038` | 403 | Custom CSS requires the Growth plan or above — below that the runtime strips `customCss` at delivery, so the write is refused instead of silently stored (echoing a stored value back, or clearing it, always passes) | | `E1020` | 401 | API token has expired | | `E1001` | 404 | User not found | | `E1002` | 404 | Company not found | | `E1003` | 404 | Company membership not found | | `E1004` | 404 | Content or content version not found. The message distinguishes two states: a plain "Content not found" means the id never existed here; an "exists but is ARCHIVED (soft-deleted)" message means the content is in the trash — list it with `deleted=true`, restore it with `POST /content/{id}/restore` | | `E1005` | 404 | Content session not found | | `E1021` | 404 | Theme not found | | `E1022` | 404 | Attribute definition not found | | `E1024` | 404 | Event definition not found | | `E1025` | 404 | Segment not found | | `E1026` | 404 | Environment not found | | `E1033` | 404 | Unknown API route (no /v2 endpoint matches the path) | | `E0003` | 400 | Request is invalid against current domain state | | `E1017` | 400 | Request validation failed (bad body / params; may carry `issues`) | | `E1015` | 400 | Invalid `scope` filter value | | `E0022` | 409 | Cannot delete the last environment | | `E0023` | 409 | Cannot delete the primary environment — set another environment as primary first | | `E0049` | 409 | Version is published (read-only) — fork a draft with the versions endpoint first | | `E0050` | 409 | Version was modified concurrently — re-read and retry | | `E1023` | 409 | Resource conflict (e.g. duplicate `codeName`) | | `E1028` | 409 | Content is still published — unpublish from every environment before deleting | | `E1030` | 409 | Event definition already has recorded events | | `E1037` | 409 | The built-in "all" segment cannot be modified or deleted — create a condition segment for a filtered audience | | `E1036` | 409 | Predefined attribute/event definitions cannot be modified or deleted — create your own definition instead | | `E1034` | 409 | Cannot delete the default theme — set another theme as the project default first | | `E1035` | 409 | System themes cannot be modified or deleted — duplicate one into your own theme (setting one as the project default is allowed) | | `E1031` | 409 | Theme is used by live or draft content — switch that content to another theme first | | `E1027` | 422 | Content version is not publishable (fails render-usability validation) | | `E1013` | 429 | Rate limit exceeded — on Cloud the limit follows your plan (100/500/1000/3000 requests/min); self-hosted deployments have a flat per-token limit instead (default 1000/min, tunable via [`API_THROTTLE_LIMIT`](/open-source/env)). The standard `Retry-After` header says how long to back off | Write endpoints validate the request body against the schema and return `E1017` with a message pointing at the offending field — for example, a malformed `data` body for a non-flow content type, or a step referencing an unknown value. Bodies are validated **strictly**: an unknown top-level key (e.g. `isPrimary` copied from a read response) is rejected with `E1017`, never silently dropped. A body that is not valid JSON at all is also `E1017` (`Invalid JSON body: ...`). ## Pagination cursors never error An invalid or expired `cursor` does **not** return an error — the request succeeds with an empty final page: ```json theme={null} { "results": [], "next": null, "previous": null } ``` This is deliberate: a cursor the server itself issued can stop matching rows when the underlying data is deleted mid-pagination, and from the server's side that is indistinguishable from a mistyped cursor. Treating both as "you have reached the end" keeps a client that is iterating `next` links from crashing halfway through a sync. If you loop on `next`, an empty page with `next: null` is your termination signal — don't probe cursors expecting a 4xx. # Expanding objects Source: https://docs.usertour.io/api-reference-v2/expanding-objects Inline related objects and heavy fields with the expand query parameter. ## Overview Many v2 responses can inline related objects or heavy fields with the `expand` query parameter, so you avoid a second request. By default these fields are omitted (or `null`). ## Usage ```bash theme={null} # inline the version objects on a content GET /v2/projects/{projectId}/content/{id}?expand=editedVersion # decompiled steps on a content version (heavy → opt-in) GET /v2/projects/{projectId}/content/{contentId}/versions/{id}?expand=steps ``` Request multiple expansions by repeating the parameter (both forms work): ```bash theme={null} ?expand=editedVersion&expand=publishedVersion ?expand[]=editedVersion&expand[]=publishedVersion ``` Comma-separated values (`?expand=editedVersion,publishedVersion`) are **not** supported — pass each value as its own `expand` parameter. ## What each resource supports | Resource | `expand` options | | --------------- | -------------------------------------------------- | | Content | `editedVersion`, `publishedVersion` | | Content version | `questions`, `steps`, `data` | | Content session | `answers`, `content`, `company`, `user`, `version` | | User | `companies`, `memberships`, `memberships.company` | | Company | `users`, `memberships`, `memberships.user` | | Theme | `settings`, `variations` | A **dotted** option expands one level deeper: `memberships.company` inlines each membership **and** the company on it (it implies `memberships`). The same goes for `memberships.user` on a company. For content versions, `steps` (flow body) and `data` (non-flow body) are heavy and only returned when expanded — see [Content representation](/api-reference-v2/content-representation). # Introduction Source: https://docs.usertour.io/api-reference-v2/introduction The Usertour v2 API — project-scoped, token-authenticated, and contract-first. The **v2 API** is the current Usertour REST API. It is project-scoped, authenticated with [personal API tokens](/api-reference-v2/authentication), and generated from a single source of truth (so this reference always matches the server). **Beta.** The v2 API (and its [MCP endpoint](/api-reference-v2/mcp)) is stable enough to build on, but the surface may still change while it's in Beta — breaking changes will be called out in the changelog. Feedback is welcome. Looking for the older API? See the **[legacy v1 reference](/api-reference/introduction)** (still supported for existing integrations). v2 is recommended for everything new — it adds content **authoring** (read *and* write), publishing, and an [MCP](/api-reference-v2/mcp) endpoint for AI agents. ## Base URL ```text Cloud theme={null} https://api.usertour.io ``` ```text Self-hosted theme={null} https:// ``` All requests use **HTTPS**. Every v2 path is rooted at a project: ``` /v2/projects/{projectId}/... ``` Business data (users, companies, sessions) is additionally scoped to an environment: ``` /v2/projects/{projectId}/environments/{environmentId}/... ``` ## Authentication Send a personal API token as a Bearer header: ``` Authorization: Bearer utp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Tokens carry **scopes** and are bound to one or more projects. See [Authentication](/api-reference-v2/authentication). ## Conventions * **Pagination** — list endpoints return `{ results, next, previous }` with cursor paging. See [Pagination](/api-reference-v2/pagination). * **Ordering** — `orderBy` (e.g. `-createdAt`). See [Ordering](/api-reference-v2/ordering). * **Expanding** — `expand` inlines related objects (e.g. `?expand=steps`, `?expand=editedVersion`). See [Expanding objects](/api-reference-v2/expanding-objects). * **Attributes** — custom fields on users/companies/events. See [Attributes](/api-reference-v2/attributes). * **Errors** — a stable `{ error: { code, message } }` envelope. See [Errors](/api-reference-v2/errors). ## What's different from v1 | | v1 | v2 | | ---------------- | --------------------------- | --------------------------------------------------------------------------- | | Path | `/v1/...` | `/v2/projects/{projectId}/...` | | Auth | access token (`ak_`) | personal API token (`utp_`) + scopes | | Publish state | single `publishedVersionId` | per-environment `environments[]` | | Content versions | read-only | read **and write** (steps, rules, theme, body) | | Lifecycle | — | publish / unpublish, duplicate, restore, draft versions | | Authoring | — | a stable [content representation](/api-reference-v2/content-representation) | | AI | — | [MCP](/api-reference-v2/mcp) endpoint | # MCP server Source: https://docs.usertour.io/api-reference-v2/mcp Drive Usertour from an AI agent over the Model Context Protocol. Usertour exposes a [Model Context Protocol](https://modelcontextprotocol.io) endpoint so AI agents (Claude, Cursor, …) can read and write your project through the same v2 services the REST API uses. **See it in action:** [Build Your Onboarding with AI](/build-onboarding-with-ai) — a video of an AI assistant building a complete onboarding experience in a real app, with the exact prompts to follow along. **Beta.** The MCP endpoint is new and evolving with the [v2 API](/api-reference-v2/introduction) — tools and behavior may still change. Breaking changes will be called out in the changelog. Feedback is welcome. ## Endpoint ```text Cloud theme={null} https://mcp.usertour.io/mcp ``` ```text Self-hosted theme={null} https:///mcp ``` It speaks MCP over **Streamable HTTP**. Whichever way you connect, this is the only URL you give your client — it discovers everything else (the authorization server, the login) automatically. A connection always acts in **exactly one project** (MCP carries no project in the path): with OAuth you pick it — along with the environments and permissions the app gets — during consent; with a personal token it's the token's project. Tools are **scope-gated** — a tool is only listed and usable if your access includes its capability, so write tools need the matching write scope (`content:create`, `user:write`, `segment:create`, `theme:update`, `session:manage`, …). **Self-hosting?** Use your own API host + `/mcp`. The discovery metadata, the `WWW-Authenticate` challenge, and the consent redirect are all derived from your instance's public URLs, so behind a reverse proxy make sure forwarded headers (`X-Forwarded-Proto` / `Host`) are correct — or pin them with [`API_URL`](/open-source/env) (every public URL) or `MCP_SERVER_URL` (just the MCP endpoint) — and that [`APP_HOMEPAGE_URL`](/open-source/env) points at your real, browser-reachable app URL (the consent step redirects there). OAuth 2.1 also requires the endpoint to be reachable over **HTTPS** (loopback is exempt). ## Connect with OAuth (one-click) Clients that support remote MCP servers over OAuth (Claude Code, Claude's custom connectors, Cursor, Codex, VS Code, ChatGPT, …) need **no token to copy** — you give them the [endpoint](#endpoint) and they run the login for you. On first use the client discovers the authorization server, registers itself ([Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591)), and opens your browser to authorize. The flow is OAuth 2.1 with PKCE. The snippets below use the Cloud endpoint — self-hosting, substitute your own `/mcp` URL. **Settings → MCP** in the app shows the same per-client steps with your instance's URL pre-filled. To serve MCP on its own domain, set `MCP_SERVER_URL` to the full public endpoint — it drives the Settings display and the OAuth discovery metadata together, and that domain must also proxy `/oauth/*` and `/.well-known/oauth-*` (the shipped nginx config does). Install the [plugin](https://github.com/usertour/skills) — it registers the MCP connection **and** the authoring skills in one: ```text theme={null} /plugin marketplace add usertour/skills /plugin install usertour@usertour ``` Then run `/mcp` inside Claude Code to authorize — the Usertour authorization screen opens in your browser. Prefer the MCP connection only (without the skills)? One shell command: ```bash theme={null} claude mcp add --transport http usertour https://mcp.usertour.io/mcp ``` **Self-hosting?** Set your server URL in the shell you launch Claude Code from (the plugin defaults to Cloud): ```bash theme={null} export USERTOUR_MCP_URL="https:///mcp" ``` One click: [**Add to Cursor**](cursor://anysphere.cursor-deeplink/mcp/install?name=usertour\&config=eyJ1cmwiOiJodHRwczovL21jcC51c2VydG91ci5pby9tY3AifQ==) — Cursor opens with the server pre-filled and walks you through authorization. Prefer manual setup? **Cursor → Settings → MCP → Add new MCP server**, or add to `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "usertour": { "url": "https://mcp.usertour.io/mcp" } } } ``` Optional: also install the authoring skill for workflow guidance: ```bash theme={null} npx skills add https://github.com/usertour/skills ``` Register the server — shared by Codex's CLI and IDE extension (not Codex Cloud): ```bash theme={null} codex mcp add usertour --url "https://mcp.usertour.io/mcp" ``` Codex supports OAuth natively and prompts you to authorize the first time you use a Usertour tool. To authorize right away instead: ```bash theme={null} codex mcp login usertour ``` Optional: also install the authoring skill for workflow guidance: ```bash theme={null} npx skills add https://github.com/usertour/skills ``` One click: [**Add to VS Code**](vscode:mcp/install?%7B%22name%22%3A%20%22usertour%22%2C%20%22type%22%3A%20%22http%22%2C%20%22url%22%3A%20%22https%3A//mcp.usertour.io/mcp%22%7D) — VS Code prompts to install the server with the values pre-filled. Prefer manual setup? Open the Command Palette and run **MCP: Open User Configuration** to open your `mcp.json`, then add this entry — the root key is `servers`, **not** `mcpServers` (the #1 copy-paste mistake coming from a Cursor/Claude config): ```json theme={null} { "servers": { "usertour": { "type": "http", "url": "https://mcp.usertour.io/mcp" } } } ``` Save — the server is available next time you chat with Copilot in Agent mode. First use prompts you to authorize. Open [chatgpt.com/plugins](https://chatgpt.com/plugins), enable **Developer mode**, and click **New Plugin** (requires a paid ChatGPT plan). Set the name (`Usertour`) and the MCP Server URL (`https://mcp.usertour.io/mcp`), leave Authentication on **OAuth**, and submit — ChatGPT opens the Usertour authorization screen. Approve to connect. Open [Claude's connector settings](https://claude.ai/new#settings/customize-connectors) and click **Add → Add custom connector**. Set the name (`Usertour`), paste the Server URL (`https://mcp.usertour.io/mcp`), and click **Add** — Claude opens the Usertour authorization screen. Pick the project, environments, and access level, then approve. Any client that reads an `mcpServers` config (Continue, Zed, …): add the entry below. The authorization flow runs automatically on first use. ```json theme={null} { "mcpServers": { "Usertour": { "url": "https://mcp.usertour.io/mcp" } } } ``` ### Authorizing When the browser opens, you sign in to Usertour (if you aren't already) and the consent screen lets you shape exactly what the app gets: * **Project** — the one project the connection may act in (fixed when you only have one). * **Environments** — which of that project's environments it can act on. Environment-targeted permissions (publishing, end-user data, sessions, …) require at least one; with several environments none are pre-selected, so handing an agent Production is always an explicit choice. * **Permissions** — what the app asked for, capped by your role on that project. Everything grantable starts checked; uncheck what you don't want to hand over, or flip **Read-only** to drop every write in one click. The connection then acts **as you, within that grant** — the grant is a ceiling under your role, never an extension of it: if your role is later downgraded or you are removed from the project, the connection loses that access immediately, exactly like a personal key. ## Managing connected apps Every app you've authorized is listed under **Settings → Connected apps**, with the project it can act in, the access it was granted, and when it was last used. **Revoke** cuts it off immediately — its tokens stop working on the next call. ## Connect with a personal token Clients that don't support OAuth (or stdio-only clients) use a personal token. Bridge to the HTTP endpoint with `mcp-remote`: ```json theme={null} { "mcpServers": { "usertour": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.usertour.io/mcp", "--header", "Authorization: Bearer utp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ] } } } ``` Use a single-project token with only the scopes the agent needs. To let it author, include the write scopes — otherwise only read tools appear. ## Tools The server ships a routing map as MCP **server instructions** in the initialize handshake — which tool serves which intent, and to read `get_authoring_guide` before authoring — so an agent starts oriented without spending a tool call. Every tool carries MCP **annotations** (`readOnlyHint` / `destructiveHint`) so a client can gate calls: read tools run freely, while destructive writes (`delete_*`, `unpublish_*`, `end_session`, …) are flagged so the client can ask for confirmation first. **Read** | Group | Tools | | --------------------- | ------------------------------------------------------------------------------- | | Guide & schemas | `get_authoring_guide`, `get_content_schema`, `get_theme_schema` | | Diagnosis | `diagnose_content`, `diagnose_user` | | Content | `list_content`, `get_content`, `list_publish_history` | | Analytics | `get_content_analytics`, `get_content_question_analytics`, `get_usage_overview` | | Versions | `list_content_versions`, `get_content_version`, `validate_content_version` | | Users | `list_users`, `get_user` | | Companies | `list_companies`, `get_company` | | Segments | `list_segments`, `get_segment` | | Sessions | `list_sessions`, `get_session` | | Themes | `list_themes`, `get_theme` | | Attribute definitions | `list_attribute_definitions`, `get_attribute_definition` | | Event definitions | `list_event_definitions`, `get_event_definition` | | Environments | `list_environments`, `get_environment` | | Webhooks | `list_webhooks` | | References | `list_references` | Every `list_*` tool pages with `cursor` / `limit`. The named-resource lists — `list_content`, `list_segments`, `list_themes`, `list_environments`, `list_event_definitions`, `list_attribute_definitions` — also take a **`name`** filter (case-insensitive substring against the display name). `list_content` additionally filters by `type`, `published`, and a created-at range; `list_segments` by `bizType`; `list_sessions` by `completed`. `get_authoring_guide` returns the in-band conventions for building usable content (lifecycle, step types, goto-by-key, the markdown subset, per-type requirements) — an agent should read it before authoring. `get_content_version` with `expand: ["steps"]` returns the decompiled steps — read them before editing with `update_content_version`. `diagnose_content` answers "why isn't my content showing?" by evaluating the SAME runtime gates the SDK uses (published / user identified / start rules / frequency / session state) as a per-gate checklist; `list_references` answers "who still uses this attribute / event / segment / theme / content" before you delete it. **Write** | Group | Tools | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Content | `create_content`, `update_content`, `delete_content`, `restore_content`, `duplicate_content`, `publish_content`, `unpublish_content` | | Versions | `create_content_version`, `update_content_version`, `restore_content_version` | | Users | `upsert_user`, `delete_user` | | Companies | `upsert_company`, `delete_company`, `add_company_member`, `remove_company_member` | | Segments | `create_segment`, `update_segment`, `delete_segment`, `add_segment_member`, `remove_segment_member` | | Themes | `create_theme`, `update_theme`, `delete_theme` | | Attribute definitions | `create_attribute_definition`, `update_attribute_definition`, `delete_attribute_definition` | | Event definitions | `create_event_definition`, `update_event_definition`, `delete_event_definition` | | Sessions | `end_session`, `delete_session` | | Environments | `create_environment`, `update_environment`, `delete_environment` | | Webhooks | `create_webhook`, `update_webhook`, `delete_webhook` — see [Webhooks](/developers/webhooks) | Content write tools take the same [representation](/api-reference-v2/content-representation) as the REST endpoints (markdown blocks, `{{ attribute }}` placeholders, rules); `update_content_version` carries `steps`, `startRules` / `hideRules`, `themeId`, and `data`. The rest mirror their REST bodies one-to-one. Give each step a `key` and wire `goto_step` to it to author a multi-step flow (forward or cyclic links) in a single call — see [step identifiers](/api-reference-v2/content-representation). Theme `settings` are writable as a **partial patch**: send only the fields you change (colors, fonts, sizes, …) — they're field-merged onto the current settings, and "Auto" hover/active colors are derived server-side. Conditional `variations` are writable too, with the same condition set the theme builder's variation editor offers. Call `get_theme_schema` for the exact writable fields and their ranges; media assets (avatars, logo, custom icons) are still set in the theme builder. `publish_content` rejects content that wouldn't render — no theme, a tooltip step with no target, an empty checklist, a launcher with no anchor, and so on. Call `validate_content_version` after authoring and before publishing to get the list of `{ ok, errors, warnings }`; `errors` are exactly what blocks publish. ## Read-only by default (prompt-injection safety) Untrusted text reaches an agent through **read** tools — session answers and custom user/company attributes can contain anything, including instructions. To stop an injected instruction from triggering a write, **give any agent that reads untrusted data read-only access** — flip **Read-only** on the OAuth consent, or mint a token with only read scopes: with no write scopes, no write tools are listed, so there is nothing to exploit. Mint a separate write-scoped token only for trusted automation. ## Troubleshooting (self-hosted) ```text theme={null} Error POSTing to endpoint: ...

405 Not Allowed

... ``` The client is POSTing MCP requests to your app's web root, which serves the admin UI and rejects POST. Almost always the configured server URL is missing the **`/mcp` path** — it must be the full endpoint (`https://your-usertour-host/mcp`), not the bare domain. Copy it from **Settings → MCP**, which shows the exact URL. ```text theme={null} SDK auth failed: Protected resource http://your-host/mcp does not match expected https://your-host/mcp (or origin) ``` Your instance described itself with `http://` URLs while the client connected over `https://`: the `https` scheme was lost on the way in, usually because a TLS-terminating proxy in front of the instance (a PaaS edge, Cloudflare, a load balancer) didn't get `X-Forwarded-Proto: https` through the whole chain. Two fixes, either works: * **Pin the URL** (simplest): set [`API_URL`](/open-source/env) to your public `https://` base — it pins every public URL the instance hands out — or `MCP_SERVER_URL` to pin just the MCP endpoint. * **Fix the header chain**: make sure every hop forwards `X-Forwarded-Proto` and `Host` unchanged, and that you run a current Usertour image. If your instance is genuinely served over plain HTTP (no TLS anywhere), MCP authorization cannot work at all — OAuth 2.1 requires HTTPS, with only localhost exempt. # Ordering Source: https://docs.usertour.io/api-reference-v2/ordering Order list results with the orderBy query parameter. ## Query parameters The field(s) to sort by. Provide a single field or an array. Multiple fields sort left-to-right. Ascending by default; prefix with `-` for descending. Each endpoint documents the fields it can be ordered by. ## Examples ```bash theme={null} # newest content first GET /v2/projects/{projectId}/content?orderBy=-createdAt ``` ```bash theme={null} # multiple fields (array syntax) GET /v2/projects/{projectId}/attribute-definitions?orderBy[]=displayName&orderBy[]=-createdAt ``` # Pagination Source: https://docs.usertour.io/api-reference-v2/pagination v2 list endpoints share one cursor-based pagination format and list object. ## Query parameters Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. ## The list object Every list endpoint returns a standardized object: The requested resource objects. Never more than `limit` items. URL of the next page (carries the `cursor`), or `null` if there is no next page. URL of the previous page, or `null` on the first page. ```json theme={null} { "results": [ { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "type": "flow", "..." : "..." }, { "id": "cm9jaftye007yrjlx3ghggigi", "object": "content", "type": "checklist", "..." : "..." } ], "next": "/v2/projects/{projectId}/content?limit=2&cursor=cm9jaftye007yrjlx3ghggigi", "previous": null } ``` To page, request the URL in `next` (or pass its `cursor` back as the `cursor` query param). # Start, hide & trigger rules Source: https://docs.usertour.io/api-reference-v2/rules When a content auto-starts, when it hides, and the in-step triggers — all built from conditions and actions. Version-level **start** and **hide** rules, plus per-step **triggers**, are how content reacts. They reuse [conditions and actions](/api-reference-v2/conditions-and-actions). Set them on the version with `PATCH …/versions/{id}` (`startRules`, `hideRules`; triggers live on each step). ## Start rules When the content auto-starts, and how often. | Field | Type | Required | Description | | -------------------- | ------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `when` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | Auto-start when these match. A **full replacement** when present; omit it to keep the stored conditions and patch only the settings (frequency / priority / …). | | `frequency` | [Frequency](#frequency) | | How often it may show. | | `priority` | `highest` \| `high` \| `medium` \| `low` \| `lowest` | | Ordering when several could start at once. | | `waitSeconds` | number | | Delay in **seconds** (not ms) before the content becomes eligible after the conditions first match. The countdown survives the conditions un-matching mid-wait, but an elapsed wait guarantees nothing — the conditions are **re-checked at show time**, and the content starts at the next moment they match again (unlike a trigger wait, which fires its actions regardless). Capped at 300 by the runtime. | | `startIfNotComplete` | boolean | | Only start if the user hasn't completed it. | Send `startRules: null` to clear them. ### Frequency | Field | Type | Required | Description | | --------- | ----------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `once` \| `multiple` \| `unlimited` | yes | `once` = show a single time; `multiple` = up to N per window; `unlimited` = every time the conditions match. Omitting `frequency` on a write seeds the builder default (`once`) — stored explicitly, visible on read-backs. A version stored with NO frequency (legacy data only) runs with NO limit and re-starts whenever its rules match again. | | `every` | `object{ times, duration, unit }` | | Re-show window — used by `multiple` (with `times`) and `unlimited`; ignored by `once`. `unit` ∈ `seconds`/`minutes`/`hours`/`days`. Manual and programmatic starts also count toward the `multiple` limit. | | `atLeast` | `object{ duration, unit }` | | Quiet period: only auto-start if no OTHER content of the SAME type has been shown within this window — and since only flows accept this knob, in practice: no other FLOW. A banner / checklist / launcher showing does NOT block it. | ```json theme={null} { "when": [{ "type": "current_url", "includes": ["/dashboard"] }], "frequency": { "mode": "once" }, "priority": "medium" } ``` ## Hide rules When to hide the content (even if started). Hiding **suspends** the session rather than ending it — when the conditions stop matching, the same session reappears at the same step. | Field | Type | Required | Description | | ------ | ------------------------------------------------------------------- | -------- | ----------------------- | | `when` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | yes | Hide while these match. | Send `hideRules: null` to clear them. ## Step triggers A step can carry **triggers** — run actions when conditions become true while the step is showing (e.g. advance when an element appears). Triggers live on the step's `triggers[]` (see [Content representation](/api-reference-v2/content-representation#steps-flow)). | Field | Type | Required | Description | | ------------- | ------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `when` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | Fire when these match. | | `do` | [Action](/api-reference-v2/conditions-and-actions#actions)\[] | yes | Actions to run. | | `waitSeconds` | number | | Delay in **seconds** (not ms) between `when` matching and `do` firing. The match is latched: the actions fire after the wait even if the conditions have since stopped matching. Capped at 300. | ```json theme={null} { "when": [{ "type": "element", "target": { "selector": "#welcome-modal" }, "state": "present" }], "do": [{ "type": "goto_step", "step": "step-2" }] } ``` # Type-specific data Source: https://docs.usertour.io/api-reference-v2/type-data The `data` body for non-flow content — checklist, launcher, banner, tracker, resource center, and announcement. Non-flow content carries its body in `data` (read with `?expand=data`, write with the `data` field of `PATCH …/versions/{id}`). A newly created content is seeded with its type's default `data`, and writes are a **field-level merge**, so you only send the fields you want to change. Nested rich content reuses [blocks](/api-reference-v2/blocks); conditions/actions reuse [rules](/api-reference-v2/conditions-and-actions). ## checklist | Field | Type | Required | Description | | ----------------- | ------------------------------------ | -------- | ------------------------------------ | | `buttonText` | string | | The launcher button label. | | `initialDisplay` | `expanded` \| `button` | | Open, or collapsed to a button. | | `completionOrder` | `any` \| `ordered` | | Whether items must be done in order. | | `preventDismiss` | boolean | | Disallow dismissing. | | `autoDismiss` | boolean | | Dismiss when all items complete. | | `content` | [Block](/api-reference-v2/blocks)\[] | | Header/intro content. | | `items` | [Item](#checklist-item)\[] | | The checklist tasks. | ### Checklist item | Field | Type | Required | Description | | -------------- | ------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | string | | Field-merge write handle (omit to add a new item). | | `name` | string | yes | Item label. | | `description` | string | | Sub-text. | | `completeWhen` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | Marks the item complete when these match. Also accepts the parameterless `{ "type": "task_clicked" }` (complete when the item is clicked), including inside an OR group. | | `clickActions` | [Action](/api-reference-v2/conditions-and-actions#actions)\[] | | Run when the item is clicked. | | `onlyShowWhen` | [Condition](/api-reference-v2/conditions-and-actions#conditions)\[] | | Only show the item while these match. | An item needs a `name` **and** either a `clickActions` or a `completeWhen`. ```json theme={null} { "buttonText": "Get started", "initialDisplay": "expanded", "items": [ { "name": "Create your first project", "clickActions": [{ "type": "navigate", "url": "/projects/new" }], "completeWhen": [{ "type": "task_clicked" }] }, { "name": "Invite a teammate", "completeWhen": [{ "type": "event", "event": "member_invited" }] } ] } ``` ## launcher | Field | Type | Required | Description | | ------------ | ---------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `style` | `beacon` \| `icon` \| `hidden` \| `button` | | Visual style. | | `icon` | `object{ source, url, type }` | | Icon config. | | `buttonText` | string | | Label (for `button` style). | | `target` | [Target](/api-reference-v2/conditions-and-actions#target) | | Element the launcher anchors to. | | `zIndex` | integer | | Stacking order. | | `tooltip` | `object{ placement, width, reference, content, settings }` | | Tooltip shown on activation — `content` is [Block](/api-reference-v2/blocks)\[]. | | `behavior` | `object{ triggerElement, event, action, actions }` | | What activates it and what it does — `actions` is [Action](/api-reference-v2/conditions-and-actions#actions)\[]. | A launcher needs a `target`; show-tooltip behavior needs tooltip `content`, perform-action behavior needs `actions`. ```json theme={null} { "style": "icon", "target": { "selector": "[data-tour='help']" }, "tooltip": { "content": [{ "type": "text", "markdown": "Need help? Start here." }] } } ``` ## banner | Field | Type | Required | Description | | ----------------- | ---------------------------------------------------------------------------- | -------- | -------------------------------- | | `placement` | [Placement](/api-reference-v2/content-representation) | | Where the banner sits. | | `content` | [Block](/api-reference-v2/blocks)\[] | | Banner content. | | `zIndex` | integer | | Stacking order. | | `settings` | `object{ overlayOverAppContent, stickToTop, allowDismiss, animateOnAppear }` | | Behavior toggles. | | `containerTarget` | [Target](/api-reference-v2/conditions-and-actions#target) | | For element-relative placements. | | `layout` | `object{ maxContentWidth, maxEmbedWidth, borderRadius, outerMargin }` | | Sizing. | A banner needs `content`; element-relative placements need a `containerTarget`. ```json theme={null} { "content": [{ "type": "text", "markdown": "🎉 New feature live!" }], "settings": { "stickToTop": true, "allowDismiss": true } } ``` ## tracker A tracker has no UI — it records an event when its [start rules](/api-reference-v2/rules) match. (No theme needed.) | Field | Type | Required | Description | | ------- | ------ | -------- | -------------------- | | `event` | string | yes | Event code to track. | ```json theme={null} { "event": "activated" } ``` ## resource-center | Field | Type | Required | Description | | ------------ | ------------------------------------- | -------- | ------------------------------------ | | `buttonText` | string | | Launcher button label. | | `headerText` | string | | Panel header. | | `tabs` | `object{ id, name, icon, blocks }`\[] | | Tabs — each has a name and ≥1 block. | Tab blocks use the resource center's **own** vocabulary — `richtext`, `divider`, `action`, `sub-page`, `content-list`, `live-chat`, and `announcement` (the feed; at most one per resource center) — **not** the flow blocks. Rich text goes inside a `richtext` block, which wraps regular [blocks](/api-reference-v2/blocks) in its `content`; a bare flow `text` block at the tab level is rejected. A resource center needs ≥1 tab, each with a name and at least one block. ```json theme={null} { "headerText": "Help & resources", "tabs": [ { "name": "Guides", "blocks": [ { "type": "richtext", "content": [{ "type": "text", "markdown": "## Getting started" }] }, { "type": "content-list", "name": "Tours", "items": [{ "content": "cm9f6vwed0002iejc4vg2zu3t", "contentType": "flow" }] } ] } ] } ``` ## announcement The announcement-feed body. Announcements reach users **only** through a resource center with an `announcement` block — publishing alone does not surface them. | Field | Type | Required | Description | | ---------------- | ------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | string | | Feed-row / detail title. **Required to publish.** Plain string — no `{{ }}` interpolation. Seeded from the content name at create, then independent. | | `introContent` | [Block](/api-reference-v2/blocks)\[] | | Feed-row content — the flow block vocabulary minus questions (button actions: `start_content` / `navigate` / `run_javascript` only). | | `enableReadMore` | boolean | | Adds a "Read more" button opening a detail page. Enabling it with an empty `detailContent` is rejected. | | `readMoreLabel` | string | | The button's label. | | `detailContent` | [Block](/api-reference-v2/blocks)\[] | | The detail page (same block rules as `introContent`). | | `distribution` | `silent` \| `badge` \| `popup` | | How loudly users are notified (`badge` is the default). | The "announcement time" is **not** in `data` — it is the version-level `scheduledAt` field (the feed hides the announcement until that instant and orders by it, newest first). ```json theme={null} { "title": "Dark mode is here", "introContent": [{ "type": "text", "markdown": "Flip it on in **Settings → Appearance**." }], "distribution": "badge" } ``` # Get content analytics Source: https://docs.usertour.io/api-reference/analytics/get-content-analytics /api-reference-v2/openapi.json get /v2/projects/{projectId}/content/{id}/analytics The response shape follows the content type (discriminated on `contentType`): flows report starts + completions and a per-step funnel with tooltip-target-missing counts; checklists starts + completions and per-task rows; launchers seen + activations; banners seen + dismissals; resource centers opens + block clicks; trackers users + occurrences of the tracked event. All with a per-day series. Defaults to the last 30 days, UTC. # Get question analytics Source: https://docs.usertour.io/api-reference/analytics/get-question-analytics /api-reference-v2/openapi.json get /v2/projects/{projectId}/content/{id}/analytics/questions Per-question aggregates for survey questions in this content: answer distribution, NPS score with promoter/passive/detractor shares, rating averages — each with a rolling-window daily series (CUMULATIVE over the trailing `rollingWindowDays`, echoed per series — unlike the content-analytics per-day byDay). Defaults to the last 30 days, UTC. # Create an attribute definition Source: https://docs.usertour.io/api-reference/attribute-definitions/create-an-attribute-definition /api-reference-v2/openapi.json post /v2/projects/{projectId}/attribute-definitions # Delete an attribute definition Source: https://docs.usertour.io/api-reference/attribute-definitions/delete-an-attribute-definition /api-reference-v2/openapi.json delete /v2/projects/{projectId}/attribute-definitions/{id} # Get an attribute definition Source: https://docs.usertour.io/api-reference/attribute-definitions/get-an-attribute-definition /api-reference-v2/openapi.json get /v2/projects/{projectId}/attribute-definitions/{id} # List attribute definitions Source: https://docs.usertour.io/api-reference/attribute-definitions/list Get /v1/attribute-definitions Retrieves a paginated list of attribute definitions in your account. Each definition includes its data type, scope, and metadata. This endpoint returns a paginated list of attribute definitions. Each definition object contains its data type, scope, and metadata. ## Query parameters Filter definitions by their scope. Available scopes: * `eventDefinition`: Event-related attributes * `company`: Company-related attributes * `companyMembership`: Company membership attributes * `user`: User-related attributes Filter definitions by event name(s). You can provide: * Single value: `eventName=page_viewed` * Multiple values: `eventName[]=page_viewed&eventName[]=clicked` Sort the results by one or more fields. Available fields: * `createdAt`: Creation timestamp * `codeName`: Technical name * `displayName`: Display name Examples: * Single field: `orderBy=createdAt` or `orderBy=-createdAt` (descending) * Multiple fields: `orderBy[]=-createdAt&orderBy[]=name` Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing an array of [attribute definition objects](/api-reference/attribute-definitions/model) in the `results` property. The response includes pagination information in the `next` and `previous` fields. ```bash Request theme={null} # List attribute definitions for specific events curl https://api.usertour.io/v1/attribute-definitions?scope=eventDefinition&eventName%5B%5D=page_viewed&eventName%5B%5D=flow_started&limit=3 \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "cm9e922or000jmjaox6ynbld2", "object": "attributeDefinition", "createdAt": "2025-04-12T13:26:30.939Z", "dataType": "string", "description": "Usertour flow id", "displayName": "Flow ID", "codeName": "flow_id", "scope": "eventDefinition" }, { "id": "cm9e922or000kmjao3nhw7w74", "object": "attributeDefinition", "createdAt": "2025-04-12T13:26:30.939Z", "dataType": "string", "description": "Usertour flow name", "displayName": "Flow Name", "codeName": "flow_name", "scope": "eventDefinition" }, { "id": "cm9e922or000lmjao49o5mwwf", "object": "attributeDefinition", "createdAt": "2025-04-12T13:26:30.939Z", "dataType": "string", "description": "A session of a specific user viewing a specific flow", "displayName": "Flow Session ID", "codeName": "flow_session_id", "scope": "eventDefinition" } ], "next": "/v1/attribute-definitions?scope=eventDefinition&eventName%5B%5D=page_viewed&eventName%5B%5D=flow_started&limit=3&cursor=cm9e922or000lmjao49o5mwwf", "previous": null } ``` # List attribute definitions Source: https://docs.usertour.io/api-reference/attribute-definitions/list-attribute-definitions /api-reference-v2/openapi.json get /v2/projects/{projectId}/attribute-definitions # The attribute definition object Source: https://docs.usertour.io/api-reference/attribute-definitions/model Attribute definitions are metadata objects that describe the structure and purpose of attributes in your application. They are automatically created when you send new attributes through the Usertour API or SDK. These definitions serve as a schema registry for your attributes, ensuring consistency in attribute usage across your application. While you can't directly manage attribute definitions through the API, they are automatically maintained as you use attributes. A unique identifier for the attribute definition. The object type identifier. Always set to "attributeDefinition" to distinguish it from other API objects. The timestamp when the attribute definition was first created, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). The data type of the attribute value. For custom attributes, see [Attribute data types](/api-reference/attributes) for supported values. A technical description of the attribute's purpose and context. This field is editable in the Usertour Dashboard and helps maintain documentation of your attribute implementation. The human-readable identifier for the attribute, used in the Usertour Dashboard and analytics interfaces. This can be modified to better reflect your attribute naming conventions. The immutable identifier used in your code when setting attribute values. This value is used in the attributes object when creating or updating users, and cannot be changed after creation to maintain data consistency. Defines which objects this attribute can be associated with. Supported values: * eventDefinition * company * companyMembership * user ```json Example attribute definition object theme={null} { "id": "cm9e922or000mmjaopovxm7kv", "object": "attributeDefinition", "createdAt": "2025-04-12T13:26:30.939Z", "dataType": "string", "description": "Describes why a flow started", "displayName": "Flow Start Reason", "codeName": "flow_start_reason", "scope": "eventDefinition" } ``` # Update an attribute definition Source: https://docs.usertour.io/api-reference/attribute-definitions/update-an-attribute-definition /api-reference-v2/openapi.json patch /v2/projects/{projectId}/attribute-definitions/{id} # Attributes Source: https://docs.usertour.io/api-reference/attributes Objects in the API, such as users, companies, and events, can store custom attributes in the ```attributes``` object. This allows you to extend the default data model with your own fields. ## Attribute naming Attribute names must follow these rules: * Only use alphanumeric characters (a-z, A-Z, 0-9) * Can include underscores, dashes, and spaces * Are case-sensitive * Must be unique within the `attributes` object We recommend using `snake_case` for all attribute names to maintain consistency across your application. For example: * `user_id` * `created_at` * `last_login_time` You can configure human-friendly display names for attributes in the Usertour UI. For instance, `signed_up_at` can be displayed as "Signed Up" in the interface. ## Attribute data types We support the following attribute data types: | Type | Description | | ---------- | ------------------------------------------------------------------------ | | `string` | Represents a string. | | `number` | Represents a number (supports both integers and floating point numbers). | | `boolean` | Represents either `true` or `false`. | | `datetime` | Represents a point in time, always stored as ISO 8601 in UTC. | | `list` | Represents a list of strings. | ## Best practices 1. **Naming conventions** * Use `snake_case` for all attribute names * Choose descriptive names that clearly indicate the data's purpose * Avoid using reserved words or system field names 2. **Data types** * Use the most appropriate data type for your data * Store dates as `datetime` type, not as strings * Use `boolean` for true/false values instead of strings 3. **Performance considerations** * Keep attribute names concise but descriptive * Avoid storing large amounts of data in attributes * Use appropriate data types to optimize storage and query performance # Add a company member Source: https://docs.usertour.io/api-reference/companies/add-a-company-member /api-reference-v2/openapi.json put /v2/projects/{projectId}/environments/{environmentId}/companies/{id}/memberships/{userId} Add a user to the company, or update the membership (idempotent). Returns the membership (external ids + attributes). Membership attributes with an unknown codeName auto-create a definition (dataType inferred from the value) — a typo'd codeName therefore silently creates a new attribute while the real one is not updated. # Upsert a company Source: https://docs.usertour.io/api-reference/companies/create Post /v1/companies Create a new company or update an existing one. This endpoint uses an upsert operation, creating a new company if the ID doesn't exist, or updating the existing company if it does. The create/update endpoint provides a unified way to manage companies in your system. When you send a request: * If the company ID doesn't exist, a new company will be created * If the company ID exists, the provided attributes will be merged with existing ones * Existing attributes not included in the request will remain unchanged Companies can also be created or updated indirectly through: * User creation/update operations with embedded company data * Membership creation/update operations ## Request body Unique identifier for the company. We recommend using a consistent prefix (e.g., `comp_`) and matching the ID from your system for easier tracking and management. A map of company attributes to create or update. You can include any custom attributes to describe the company. See [Attributes](/api-reference/attributes) for detailed information about attribute types and best practices. ## Response Returns the created or updated [company object](/api-reference/companies/model) with all its fields and relationships. ```bash Request theme={null} curl https://api.usertour.io/v1/companies \ -X POST \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' \ -d '{ "id": "comp_123456789", "attributes": { "name": "Acme Corporation", "industry": "Technology", "employee_count": 500, "subscription_tier": "enterprise", "billing_country": "US", "founded_at": "2020-01-01T00:00:00.000Z" } }' ``` ```json Response theme={null} { "id": "comp_123456789", "object": "company", "attributes": { "name": "Acme Corporation", "industry": "Technology", "employee_count": 500, "subscription_tier": "enterprise", "billing_country": "US", "founded_at": "2020-01-01T00:00:00.000Z" }, "createdAt": "2024-03-20T08:30:00.000Z", "memberships": null, "users": null } ``` # Create or update a company Source: https://docs.usertour.io/api-reference/companies/create-or-update-a-company /api-reference-v2/openapi.json put /v2/projects/{projectId}/environments/{environmentId}/companies/{id} # Delete a company Source: https://docs.usertour.io/api-reference/companies/delete Delete /v1/companies/:id Permanently remove a company from the system. This operation cannot be undone, and all associated memberships will be deleted. This endpoint permanently deletes a company and its associated data. When a company is deleted: * All company memberships are automatically removed * The company's attributes and relationships are permanently deleted * The operation cannot be reversed ## Path parameters The unique identifier of the company to delete. This should match the ID format used in your system (e.g., `comp_123456789`). ## Response Returns a confirmation object indicating the successful deletion of the company. The unique identifier of the deleted company. String representing the object's type. Always set to `"company"`. Indicates the success of the deletion operation. Always `true` when the request succeeds. ```bash Request theme={null} curl https://api.usertour.io/v1/companies/comp_123456789 \ -X DELETE \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "comp_123456789", "object": "company", "deleted": true } ``` # Delete a company Source: https://docs.usertour.io/api-reference/companies/delete-a-company /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{environmentId}/companies/{id} DESTRUCTIVE and permanent — removes the company, every membership in it (with their membership attributes) and its segment rows. Users themselves survive. There is no restore; re-grouping the same external id later starts a brand-new company. # Get a company Source: https://docs.usertour.io/api-reference/companies/get Get /v1/companies/:id Retrieve detailed information about a specific company. The endpoint returns a 404 Not Found response if the company does not exist. This endpoint retrieves a single company's information from the system. You can use the `expand` query parameter to include related objects such as memberships and users in the response. ## Path parameters The unique identifier of the company to retrieve. This should match the ID format used in your system (e.g., `comp_123456789`). ## Response Returns the [company object](/api-reference/companies/model) with its current state, including: * Company metadata and attributes * Creation timestamp * Related memberships and users (if expanded) ```bash Request theme={null} curl https://api.usertour.io/v1/companies/comp_123456789 \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "comp_123456789", "object": "company", "attributes": { "name": "Acme Corporation", "industry": "Technology", "employee_count": 500, "subscription_tier": "enterprise", "billing_country": "US", "founded_at": "2020-01-01T00:00:00.000Z" }, "createdAt": "2024-03-20T08:30:00.000Z", "users": null, "memberships": null } ``` # Get a company Source: https://docs.usertour.io/api-reference/companies/get-a-company /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/companies/{id} # List companies Source: https://docs.usertour.io/api-reference/companies/list Get /v1/companies Retrieve a paginated list of companies in your account. The response includes company details and pagination information. This endpoint returns a paginated list of companies associated with your account. Each company object includes its attributes, relationships, and metadata. ## Query parameters Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. See [Expanding objects](/api-reference/expanding-objects). Available options: * `memberships`: Include basic membership data for each company * `memberships.user`: Include both membership and associated user details * `users`: Include all users associated with the company * You can request multiple expansions: `expand[]=memberships&expand[]=users` ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing an array of [company objects](/api-reference/companies/model) in the `results` property. The response includes pagination information in the `next` and `previous` fields. ```bash Request theme={null} curl https://api.usertour.io/v1/companies \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "comp_123456789", "object": "company", "attributes": { "name": "Acme Corp", "domain": "acme.com", "industry": "Technology", "size": "100-500", "created_at": "2024-03-20T08:30:00.000Z" }, "createdAt": "2024-03-20T08:30:00.000Z", "updatedAt": "2024-03-20T08:30:00.000Z" }, { "id": "comp_987654321", "object": "company", "attributes": { "name": "TechStart Inc", "domain": "techstart.io", "industry": "Software", "size": "50-100", "created_at": "2024-03-19T15:45:00.000Z" }, "createdAt": "2024-03-19T15:45:00.000Z", "updatedAt": "2024-03-19T15:45:00.000Z" } ], "next": "/v1/companies?limit=2&cursor=comp_987654321", "previous": null } ``` # List companies Source: https://docs.usertour.io/api-reference/companies/list-companies /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/companies # The company object Source: https://docs.usertour.io/api-reference/companies/model Companies represent organizational units in your system. They can be used to group users, manage permissions, and orchestrate user flows based on company-specific conditions. ## Overview Companies serve as organizational containers that group users together. In a business context, companies can represent: * Organizations * Teams * Departments * Business units Companies support custom attributes and can be associated with events, enabling sophisticated flow orchestration based on: * Company-wide metrics and conditions * User behavior patterns within the company * Cross-company relationships and hierarchies ## Object structure Unique identifier for the company. Must match the company's ID in your system. We recommend using a consistent prefix (e.g., `comp_`) for better identification. String representing the object's type. Always set to `"company"`. A map of company-specific attributes. You can store any custom data to describe the company, such as: * Company metadata (name, industry, size) * Business metrics (revenue, employee count) * Custom flags and settings ISO 8601 timestamp in UTC indicating when the company was created in the system. For tracking when the company was created in your application, use a custom attribute like `company_created_at` in the attributes object. An array of [membership objects](/api-reference/company-memberships/model) representing user-company relationships. Each membership can store custom attributes that describe the relationship between the user and company (e.g., join date, status, preferences). The field defaults to `null` but can be [expanded](/api-reference/expanding-objects) using: * `?expand=memberships` to include basic membership data * `?expand=memberships.user` to include both membership and user details An array of [user objects](/api-reference/users/model) representing all members of this company. Use this field when you only need user data without membership attributes. The field defaults to `null` but can be [expanded](/api-reference/expanding-objects) using `?expand=users`. ```json Example company object theme={null} { "id": "comp_123456789", "object": "company", "attributes": { "name": "Acme Corporation", "industry": "Technology", "employee_count": 500, "founded_at": "2020-01-01T00:00:00.000Z", "subscription_tier": "enterprise", "billing_country": "US" }, "createdAt": "2024-03-20T08:30:00.000Z", "users": [ { "id": "usr_987654321", "object": "user", "attributes": { "name": "John Doe", "email": "john.doe@acme.com", "role": "admin", "department": "engineering", "last_login_at": "2024-03-20T08:30:00.000Z" }, "createdAt": "2024-03-19T15:45:00.000Z" } ], "memberships": null } ``` # Remove a company member Source: https://docs.usertour.io/api-reference/companies/remove-a-company-member /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{environmentId}/companies/{id}/memberships/{userId} Remove a user from the company. DESTRUCTIVE, not a hide: the membership record and its membership-scoped attributes (e.g. a role) are deleted for good — re-adding creates a brand-new membership with empty attributes and a new join date, so remove-then-re-add is NOT an undo. # Upsert a membership Source: https://docs.usertour.io/api-reference/company-memberships/create Post /v1/company-memberships Create or update a company membership. Note that memberships are managed through user operations rather than direct API calls. Company memberships are managed indirectly through user operations. To create or update a membership: 1. Use the [create or update a user ](/api-reference/users/create) endpoints 2. Include the membership details in the `memberships` field of the user object 3. The system will automatically handle the membership creation/update This approach ensures data consistency and maintains proper relationships between users and companies. ```json Request theme={null} { "name": "John Doe", "email": "john@example.com", "memberships": [ { "company_id": "comp_123456789", "role": "admin" } ] } ``` # Remove a membership Source: https://docs.usertour.io/api-reference/company-memberships/delete Delete /v1/company-memberships Removes a single user from a single company. Leaves the user and the company themselves intact. ## Query parameters Unique identifier for the user. Should match the ID the user has in your database. Unique identifier for the group. Should match the ID the group has in your database. ## Response Returns an object with a deleted key on success (or if the user already wasn't a member of the group - this call is idempotent). The unique identifier of the membership. String representing the object's type. Always set to `"companyMembership"`. Indicates the success of the deletion operation. Always `true` when the request succeeds. ```bash Request theme={null} curl https://api.usertour.io/v1/company-memberships?userId=user-1746490725519&companyId=group-555555 \ -X DELETE \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": membership_123456789", "object": "companyMembership", "deleted": true } ``` # List memberships Source: https://docs.usertour.io/api-reference/company-memberships/list Get /v1/company-memberships Retrieve company memberships by expanding the memberships field of a user or company object. Direct listing of memberships is not supported. Company memberships are accessed through their parent objects (users or companies) using the [expansion](/api-reference/expanding-objects) feature. This design ensures proper data relationships and access control. ## Retrieving Memberships ### Through Users To get a user's memberships: 1. Fetch the user using the [user list endpoint](/api-reference/users/list) 2. Include `?expand=memberships` in the request URL 3. The response will include the expanded memberships array ```bash Request theme={null} curl https://api.usertour.io/v1/users/usr_123456789?expand=memberships \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ### Through Companies To get a company's memberships: 1. Fetch the company using the [company list endpoint](/api-reference/companies/list) 2. Include `?expand=memberships` in the request URL 3. The response will include the expanded memberships array ```bash Request theme={null} curl https://api.usertour.io/v1/companies/comp_123456789?expand=memberships \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ## Response The expanded memberships will be included in the response as an array of [membership objects](/api-reference/company-memberships/model). ```json Response theme={null} { "id": "usr_123456789", "object": "user", "attributes": { "name": "John Doe", "email": "john@example.com" }, "memberships": [ { "id": "mem_123456789", "object": "membership", "company_id": "comp_123456789", "role": "admin", "created_at": "2024-03-20T08:30:00.000Z" } ] } ``` # The company membership object Source: https://docs.usertour.io/api-reference/company-memberships/model Company memberships represent the relationship between users and companies. They allow you to store custom attributes that describe how a user interacts with a specific company. ## Overview Company memberships establish a flexible many-to-many relationship model between users and companies. This design allows: * A single user to belong to multiple companies * A company to have multiple user members * Different roles and permissions for the same user across different companies The membership object serves as the ideal place to store relationship-specific data, particularly user roles and permissions. For instance, a user might be an Owner in one company while serving as an Admin in another. This role information belongs in the membership attributes rather than the user or company objects, as it represents the specific relationship between a user and a company. Company memberships serve as the connection between users and companies, enabling you to: * Track user-company relationships * Store relationship-specific attributes * Manage user access and permissions * Track membership history and changes ## Object structure Unique identifier for the membership. The system automatically generates this ID when a new membership is created. String representing the object's type. Always set to `"companyMembership"`. A map of custom attributes that describe the user's relationship with the company. You can store any data relevant to this specific membership, such as: * Membership status and type * Access levels and permissions * Department and role information * Custom preferences and settings ISO 8601 timestamp in UTC indicating when the membership was created in the system. For tracking when the user joined your application, use a custom attribute like `joined_at` in the user's attributes object. The associated [company object](/api-reference/companies/model). This field defaults to `null` but can be expanded using `?expand=company` to include the full company details. The unique identifier of the associated company. This ID can be used to reference the company in other API calls. The associated [user object](/api-reference/users/model). This field defaults to `null` but can be expanded using `?expand=user` to include the full user details. The unique identifier of the associated user. This ID can be used to reference the user in other API calls. ```json Example company membership object theme={null} { "id": "mem_123456789", "object": "companyMembership", "attributes": { "role": "admin", "department": "engineering", "join_date": "2024-01-15T00:00:00.000Z", "access_level": "full", "status": "active" }, "createdAt": "2024-03-20T08:30:00.000Z", "company": null, "companyId": "comp_987654321", "user": null, "userId": "usr_123456789" } ``` # Delete a content session Source: https://docs.usertour.io/api-reference/content-sessions/delete Delete /v1/content-sessions/:id Permanently deletes a content session and all its associated data (progress, survey answers, etc.). This operation cannot be undone. The API is idempotent - attempting to delete a non-existent session will return a success response. ## Path Parameters The unique identifier of the content session to delete. ## Response Returns a deletion confirmation object with the following fields: The unique identifier of the deleted content session. String representing the object's type. Always set to `contentSession`. Indicates the success of the deletion operation. Always `true` when the request succeeds. ```bash Request theme={null} # Delete a content session curl https://api.usertour.io/v1/content-sessions/cmaavyi16003twg0bgimbcdnr \ -X DELETE \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "cmaavyi16003twg0bgimbcdnr", "object": "contentSession", "deleted": true } ``` # End a content session Source: https://docs.usertour.io/api-reference/content-sessions/end Post /v1/content-sessions/:id/end Marks a content session as completed and records its completion time. This operation is idempotent - calling it multiple times on the same session will not cause any issues. ## Path Parameters The unique identifier of the content session to end. ## Response Returns the updated [content session object](/api-reference/content-sessions/model) with the following changes: * `completed` set to `true` * `completedAt` set to the current timestamp * `lastActivityAt` updated to the current timestamp ```bash Request theme={null} # End a content session curl https://api.usertour.io/v1/content-sessions/cmaavyi16003twg0bgimbcdnr/end \ -X POST \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "cmaavyi16003twg0bgimbcdnr", "object": "contentSession", "answers": null, "completedAt": "2025-05-05T09:36:13.016Z", "completed": true, "contentId": "cm9f6vwed0002iejc4vg2zu3t", "content": null, "createdAt": "2025-05-05T09:36:13.002Z", "companyId": "group-1746437717425", "company": null, "isPreview": false, "lastActivityAt": "2025-05-05T09:36:13.016Z", "progress": 11, "userId": "user-1746437717384", "user": null, "versionId": "cm9mjsv7d001bvfe8qe8tgzv9", "version": null } ``` # Get a content session Source: https://docs.usertour.io/api-reference/content-sessions/get Get /v1/content-sessions/:id Retrieves a specific content session by its ID, including all its attributes and relationships. ## Path Parameters The unique identifier of the content session to retrieve. ## Query Parameters Include additional related objects in the response. Available options: * `content`: Include the associated [content object](/api-reference/content/model) * `version`: Include the [content version](/api-reference/content-versions/model) details * `answers`: Include [survey answers](/api-reference/content-sessions/the-answer-model) if any * `company`: Include the associated [company](/api-reference/companies/model) information * `user`: Include the [user](/api-reference/users/model) who created the session * You can request multiple expansions: `expand[]=content&expand[]=user` ## Response Returns a [content session object](/api-reference/content-sessions/model) if found. Returns a 404 Not Found error if the session doesn't exist. ```bash Request theme={null} # Get a content session with all related data curl https://api.usertour.io/v1/content-sessions/cmaavyi16003twg0bgimbcdnr?expand[]=content&expand[]=version&expand[]=answers&expand[]=company&expand[]=user \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "cmaavyi16003twg0bgimbcdnr", "object": "contentSession", "answers": [], "completedAt": null, "completed": false, "contentId": "cm9f6vwed0002iejc4vg2zu3t", "content": { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "name": "fff", "type": "flow", "editedVersionId": "cm9mkru2c000mlhmajx3l0146", "publishedVersionId": "cm9mjsv7d001bvfe8qe8tgzv9", "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" }, "createdAt": "2025-05-05T09:36:13.002Z", "companyId": "group-1746437717425", "company": { "id": "group-1746437717425", "object": "company", "attributes": { "name": "Iric L" }, "createdAt": "2025-05-05T09:35:17.434Z" }, "isPreview": false, "lastActivityAt": "2025-05-05T09:36:13.016Z", "progress": 11, "userId": "user-1746437717384", "user": { "id": "user-1746437717384", "object": "user", "attributes": { "like": ["123", "456"], "male": true, "name": "Iric L", "email": "iric-1746437717384@usertour.com", "sdsdd": 13, "loginName": "222222222222", "registerAt": "2024-03-29T16:05:45.000Z", "website_lead": true }, "createdAt": "2025-05-05T09:35:17.406Z" }, "versionId": "cm9mjsv7d001bvfe8qe8tgzv9", "version": { "id": "cm9mjsv7d001bvfe8qe8tgzv9", "object": "contentVersion", "number": 13, "updatedAt": "2025-04-18T08:49:26.501Z", "createdAt": "2025-04-18T08:49:26.501Z" } } ``` # List content sessions Source: https://docs.usertour.io/api-reference/content-sessions/list Get /v1/content-sessions Retrieves a paginated list of content sessions with optional filtering and expansion options. ## Query Parameters Filter sessions by a specific content ID. Only sessions associated with this content will be returned. Filter sessions by a specific user ID. Only sessions created by this user will be returned. Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. Sort the results by specified fields. Available options: * `createdAt`: Sort by session creation time (default) * Use `-` prefix for descending order: `orderBy[]=-createdAt` * You can combine multiple fields: `orderBy[]=-createdAt&orderBy[]=id` Include additional related objects in the response. Available options: * `content`: Include the associated [content object](/api-reference/content/model) * `version`: Include the [content version](/api-reference/content-versions/model) details * `answers`: Include [survey answers](/api-reference/content-sessions/the-answer-model) if any * `company`: Include the associated [company](/api-reference/companies/model) information * `user`: Include the [user](/api-reference/users/model) who created the session * You can request multiple expansions: `expand[]=content&expand[]=user` ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing: * `results`: Array of [content session objects](/api-reference/content-sessions/model) * `next`: URL for the next page (null if no more pages) * `previous`: URL for the previous page (null if on first page) ```bash Request theme={null} # Get the most recent content session with all related data curl https://api.usertour.io/v1/content-sessions?contentId=cm9f6vwed0002iejc4vg2zu3t&limit=1&expand[]=content&orderBy=-createdAt&expand[]=version&expand[]=answers&expand[]=company&expand[]=user \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "cmaavyi16003twg0bgimbcdnr", "object": "contentSession", "answers": [], "completedAt": null, "completed": false, "contentId": "cm9f6vwed0002iejc4vg2zu3t", "content": { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "name": "fff", "type": "flow", "editedVersionId": "cm9mkru2c000mlhmajx3l0146", "publishedVersionId": "cm9mjsv7d001bvfe8qe8tgzv9", "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" }, "createdAt": "2025-05-05T09:36:13.002Z", "companyId": "group-1746437717425", "company": { "id": "group-1746437717425", "object": "company", "attributes": { "name": "Iric L" }, "createdAt": "2025-05-05T09:35:17.434Z" }, "isPreview": false, "lastActivityAt": "2025-05-05T09:36:13.016Z", "progress": 11, "userId": "user-1746437717384", "user": { "id": "user-1746437717384", "object": "user", "attributes": { "like": ["123", "456"], "male": true, "name": "Iric L", "email": "iric-1746437717384@usertour.com", "sdsdd": 13, "loginName": "222222222222", "registerAt": "2024-03-29T16:05:45.000Z", "website_lead": true }, "createdAt": "2025-05-05T09:35:17.406Z" }, "versionId": "cm9mjsv7d001bvfe8qe8tgzv9", "version": { "id": "cm9mjsv7d001bvfe8qe8tgzv9", "object": "contentVersion", "number": 13, "updatedAt": "2025-04-18T08:49:26.501Z", "createdAt": "2025-04-18T08:49:26.501Z" } } ], "next": "/v1/content-sessions?contentId=cm9f6vwed0002iejc4vg2zu3t&limit=1&orderBy=-createdAt&cursor=cmaavyi16003twg0bgimbcdnr&expand%5B%5D=content&expand%5B%5D=version&expand%5B%5D=answers&expand%5B%5D=company&expand%5B%5D=user", "previous": null } ``` # The content session object Source: https://docs.usertour.io/api-reference/content-sessions/model A content session represents a user's interaction with a specific content object (flow, checklist, or launcher). It tracks the user's progress, records their survey responses, and maintains the state of their journey through the content. A unique identifier for the content session. The object type identifier. Always set to "contentSession" to distinguish it from other API objects. A collection of [content session answer objects](/api-reference/content-sessions/the-answer-model) containing the user's responses to survey questions. This field is only populated for flow sessions and can be expanded using the `?expand=answers` query parameter. The timestamp when the content was completed, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Returns null if the content is not yet completed. Indicates whether the content has been completed. For flows, completion occurs when the user reaches a goal step. For checklists, completion means all tasks have been finished. The identifier of the content object (flow, checklist, or launcher) associated with this session. The associated [content object](/api-reference/content/model). Can be expanded using the `?expand=content` query parameter. The timestamp when the session was first created, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). The identifier of the company associated with this session, if applicable. The associated [company object](/api-reference/companies/model). Can be expanded using the `?expand=company` query parameter. Indicates whether this session was initiated by a team member previewing a draft version of the content. The timestamp of the user's last interaction with the content (e.g., advancing to the next step, expanding a checklist), in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). A decimal value representing the user's progress through the content, where 1.0 indicates 100% completion. For example, 0.4 represents 40% progress. The identifier of the user who is viewing the content. The associated [user object](/api-reference/users/model). Can be expanded using the `?expand=user` query parameter. The identifier of the content version being used in this session. The associated [content version object](/api-reference/content-versions/model). Can be expanded using the `?expand=version` query parameter. ```json Example content session object theme={null} { "id": "cm9l86wtk000zvy5q38v42053", "object": "contentSession", "answers": null, "completedAt": "2025-04-17T10:36:45.933Z", "completed": true, "contentId": "cm9f6vwed0002iejc4vg2zu3t", "content": { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "name": "fff", "type": "flow", "editedVersionId": "cm9mkru2c000mlhmajx3l0146", "publishedVersionId": "cm9mjsv7d001bvfe8qe8tgzv9", "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" }, "createdAt": "2025-04-17T10:36:40.232Z", "companyId": null, "company": null, "isPreview": false, "lastActivityAt": "2025-04-17T10:36:45.933Z", "progress": 0, "userId": "1744886199938", "user": null, "versionId": "cm9l5txup007lv0gkyphk5l2j", "version": { "id": "cm9l5txup007lv0gkyphk5l2j", "object": "contentVersion", "number": 4, "updatedAt": "2025-04-17T09:30:35.794Z", "createdAt": "2025-04-17T09:30:35.794Z" } } ``` # The answer object Source: https://docs.usertour.io/api-reference/content-sessions/the-answer-model The content session answer object represents a user's response to a survey question within a content session. It contains both the answer value and metadata about the question being answered. A unique identifier for the answer. The object type identifier. Always set to "contentSessionAnswer" to distinguish it from other API objects. The type of answer. Possible values include: * "nps" - Net Promoter Score (0-10) * "star-rating" - Star rating (1-5) * "multi-line-text" - Multi-line text input * "multiple-choice" - Multiple choice selection * "single-line-text" - Single line text input * "scale" - Scale rating (1-10) The value of the answer. The format depends on the answerType: * For "nps", "star-rating", "scale": A number as a string (e.g., "2", "8") * For "multiple-choice": The selected choice option value * For "single-line-text", "multi-line-text": The text input value The timestamp when the answer was provided, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). The Cross-Version ID (CVID) of the question object that this answer corresponds to. The name of the question object that this answer corresponds to. ```json Example content session answer object theme={null} { "id": "cm9md7s3s00enurz50zx8ovad", "object": "contentSessionAnswer", "answerType": "nps", "answerValue": "8", "createdAt": "2025-04-18T05:45:05.028Z", "questionCvid": "llwjbkwqb7xufu2okq8gk7ud", "questionName": "How likely are you to recommend our service?" } ``` # Create a content version Source: https://docs.usertour.io/api-reference/content-versions/create-a-content-version /api-reference-v2/openapi.json post /v2/projects/{projectId}/content/{contentId}/versions Ensure an editable draft: returns the current edited version while it is an unpublished draft; forks it only when it is published (locked). A fork returns the slim version envelope — the copied steps/data are not inlined; read the version with `expand` to inspect them. # Get a content version Source: https://docs.usertour.io/api-reference/content-versions/get Get /v1/content-versions/:id Retrieves a specific version of a content object by its version ID. ## Path parameters The unique identifier of the content version to retrieve. ## Query parameters See [Expanding objects](/api-reference/expanding-objects). Available options: * `questions`: Include all questions associated with this version (only applicable for flow content types) ## Response Returns a [content version object](/api-reference/content-versions/model) if found. Returns a 404 Not Found error if the version doesn't exist. ```bash Request theme={null} # Get a content version with its questions curl https://api.usertour.io/v1/content-versions/cm9l2ws8o004yv0gk0r01r1ti?expand=questions \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "cm9l2ws8o004yv0gk0r01r1ti", "object": "contentVersion", "number": 3, "questions": [ { "object": "question", "cvid": "llwjbkwqb7xufu2okq8gk7ud", "name": "How likely are you to recommend our service?", "type": "nps" }, { "object": "question", "cvid": "eforyzobzloaxa439fg6w1vw", "name": "What aspects could we improve?", "type": "nps" }, { "object": "question", "cvid": "k65k0h7clxsd32x1t9ocnrbt", "name": "Additional feedback", "type": "multi-line-text" } ], "updatedAt": "2025-04-17T09:28:33.999Z", "createdAt": "2025-04-17T08:08:49.637Z" } ``` # Get a content version Source: https://docs.usertour.io/api-reference/content-versions/get-a-content-version /api-reference-v2/openapi.json get /v2/projects/{projectId}/content/{contentId}/versions/{id} # List content versions Source: https://docs.usertour.io/api-reference/content-versions/list Get /v1/content-versions Retrieves a paginated list of versions for a specific content object. ## Query parameters The ID of the content object whose versions you want to list. This parameter is required as versions can only be listed for a specific content object. Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. See [Ordering](/api-reference/ordering). Available fields: * `createdAt`: Sort by creation date (default) * You can use multiple fields and specify sort direction: `orderBy[]=-createdAt` for descending order See [Expanding objects](/api-reference/expanding-objects). Available options: * `questions`: Include all questions associated with each version (only applicable for flow content types) ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing: * `results`: Array of [content version objects](/api-reference/content-versions/model) * `next`: URL for the next page of results (null if no more pages) * `previous`: URL for the previous page of results (null if on first page) ```bash Request theme={null} # Get the first page of versions for a specific content curl https://api.usertour.io/v1/content-versions?contentId=cm9f6vwed0002iejc4vg2zu3t&limit=2&expand=questions \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "cm9f6vwei0004iejcb9uznra3", "object": "contentVersion", "number": 0, "questions": [], "updatedAt": "2025-04-13T05:13:47.373Z", "createdAt": "2025-04-13T05:13:29.797Z" }, { "id": "cm9l2ws8o004yv0gk0r01r1ti", "object": "contentVersion", "number": 3, "questions": [ { "object": "question", "cvid": "llwjbkwqb7xufu2okq8gk7ud", "name": "How likely are you to recommend our service?", "type": "nps" }, { "object": "question", "cvid": "eforyzobzloaxa439fg6w1vw", "name": "What aspects could we improve?", "type": "nps" }, { "object": "question", "cvid": "k65k0h7clxsd32x1t9ocnrbt", "name": "Additional feedback", "type": "multi-line-text" } ], "updatedAt": "2025-04-17T09:28:33.999Z", "createdAt": "2025-04-17T08:08:49.637Z" } ], "next": "/v1/content-versions?contentId=cm9f6vwed0002iejc4vg2zu3t&limit=2&expand=questions&cursor=cm9l2ws8o004yv0gk0r01r1ti", "previous": null } ``` # List content versions Source: https://docs.usertour.io/api-reference/content-versions/list-content-versions /api-reference-v2/openapi.json get /v2/projects/{projectId}/content/{contentId}/versions # The content version object Source: https://docs.usertour.io/api-reference/content-versions/model Content versions represent different iterations of your content (flows, checklists, and launchers). A new version is automatically created when you make edits or publish content, allowing you to track changes and maintain a history of your content's evolution. A unique identifier for the content version. The object type identifier. Always set to "contentVersion" to distinguish it from other API objects. The version number of the content. Version numbers are incremental for each content object, starting from 1. A collection of [question objects](/api-reference/content-versions/the-question-model) associated with this version. This field is only populated for flow content types and can be expanded using the `?expand=questions` query parameter. The timestamp when the version was last updated, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). The timestamp when the version was created, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). ```json Example content version object theme={null} { "id": "cm9ja53e9005lrjlxm3zo13g8", "object": "contentVersion", "number": 1, "questions": [ { "object": "question", "cvid": "llwjbkwqb7xufu2okq8gk7ud", "name": "How likely are you to recommend our service?", "type": "nps" }, { "object": "question", "cvid": "e79iosim51pbvy4ikq9igh5a", "name": "Rate your experience", "type": "star-rating" } ], "updatedAt": "2025-04-16T01:55:42.307Z", "createdAt": "2025-04-16T01:55:42.307Z" } ``` # Restore a content version Source: https://docs.usertour.io/api-reference/content-versions/restore-a-content-version /api-reference-v2/openapi.json post /v2/projects/{projectId}/content/{contentId}/versions/{id}/restore Fork a historical version forward as the new draft. # The question object Source: https://docs.usertour.io/api-reference/content-versions/the-question-model The question object represents a survey question within a content version. It defines the structure and type of the question, along with its metadata. A unique identifier for the question. Important: The ID of questions changes between versions. When a new version is created, all components are copied with new ID values. Use `cvid` for stable cross-version identification. The object type identifier. Always set to "question" to distinguish it from other API objects. Cross-Version ID (CVID). A stable identifier that remains constant for the same logical question across different versions. Use this instead of `id` when referencing questions, as it persists across version changes. The name of the question as defined in the Builder interface. The type of question. Supported values: * "nps" - Net Promoter Score (0-10) * "star-rating" - Star rating (1-5) * "multi-line-text" - Multi-line text input * "multiple-choice" - Multiple choice selection * "single-line-text" - Single line text input * "scale" - Scale rating (1-10) The timestamp when the question was last updated, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). The timestamp when the question was created, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). ```json Example question object theme={null} { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "question", "cvid": "llwjbkwqb7xufu2okq8gk7ud", "name": "How likely are you to recommend our service?", "type": "nps", "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" } ``` # Update a content version Source: https://docs.usertour.io/api-reference/content-versions/update-a-content-version /api-reference-v2/openapi.json patch /v2/projects/{projectId}/content/{contentId}/versions/{id} Write steps, start/hide rules, themeId, or type-specific data to a draft version. # Validate a content version Source: https://docs.usertour.io/api-reference/content-versions/validate-a-content-version /api-reference-v2/openapi.json get /v2/projects/{projectId}/content/{contentId}/versions/{id}/validate Dry-run usability check: the same rules `publish` enforces, without mutating. Returns errors (these block publish) and advisory warnings. # Create content Source: https://docs.usertour.io/api-reference/content/create-content /api-reference-v2/openapi.json post /v2/projects/{projectId}/content # Delete content Source: https://docs.usertour.io/api-reference/content/delete-content /api-reference-v2/openapi.json delete /v2/projects/{projectId}/content/{id} Soft delete (recoverable via POST /content/{id}/restore). Deleting also UNPUBLISHES the content from every environment and permanently discards the per-environment publish state (which environments, which version, since when) — restore returns an UNPUBLISHED draft, not that history. "Was this ever live?" is answerable afterwards through the publish history (available through the MCP) or the app's audit log. # Duplicate content Source: https://docs.usertour.io/api-reference/content/duplicate-content /api-reference-v2/openapi.json post /v2/projects/{projectId}/content/{id}/duplicate Duplicate into a new content (copies the edited version's steps / config / data). # Get a content Source: https://docs.usertour.io/api-reference/content/get Get /v1/content/:id Retrieves a single content object by its ID. ## Path parameters The unique identifier of the content object you want to retrieve. ## Query parameters Specify which related objects to include in the response. Available options: * `editedVersion`: Include the current draft version being edited * `publishedVersion`: Include the currently published version * You can request multiple expansions using array syntax: `expand[]=publishedVersion&expand[]=editedVersion` ## Response Returns a [content object](/api-reference/content/model) if found. Returns a 404 Not Found error if the content doesn't exist. ```bash Request theme={null} # Get a content with both draft and published versions curl https://api.usertour.io/v1/content/cm9f6vwed0002iejc4vg2zu3t?expand[]=publishedVersion&expand[]=editedVersion \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "name": "Product Onboarding Flow", "type": "flow", "editedVersionId": "cm9mkru2c000mlhmajx3l0146", "editedVersion": { "id": "cm9mkru2c000mlhmajx3l0146", "object": "contentVersion", "number": 14, "questions": [], "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-18T09:16:37.989Z" }, "publishedVersionId": "cm9mjsv7d001bvfe8qe8tgzv9", "publishedVersion": { "id": "cm9mjsv7d001bvfe8qe8tgzv9", "object": "contentVersion", "number": 13, "questions": [], "updatedAt": "2025-04-18T08:49:26.501Z", "createdAt": "2025-04-18T08:49:26.501Z" }, "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" } ``` # Get content Source: https://docs.usertour.io/api-reference/content/get-content /api-reference-v2/openapi.json get /v2/projects/{projectId}/content/{id} # List content Source: https://docs.usertour.io/api-reference/content/list Get /v1/content Retrieves a paginated list of all content objects in your account. ## Query parameters Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. See [Ordering](/api-reference/ordering). Available fields: * `createdAt`: Sort by creation date (default) * You can use multiple fields and specify sort direction: `orderBy[]=-createdAt` for descending order See [Expanding objects](/api-reference/expanding-objects). Available options: * `editedVersion`: Include the current draft version being edited * `publishedVersion`: Include the currently published version * You can request multiple expansions: `expand[]=publishedVersion&expand[]=editedVersion` ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing: * `results`: Array of [content objects](/api-reference/content/model) * `next`: URL for the next page of results (null if no more pages) * `previous`: URL for the previous page of results (null if on first page) ```bash Request theme={null} # Get the first page of content objects curl https://api.usertour.io/v1/content \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "name": "Product Onboarding Flow", "type": "flow", "editedVersionId": "cm9mkru2c000mlhmajx3l0146", "publishedVersionId": "cm9mjsv7d001bvfe8qe8tgzv9", "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" }, { "id": "cm9jaftye007yrjlx3ghggigi", "object": "content", "name": "User Setup Checklist", "type": "checklist", "editedVersionId": "cm9jaftyh0080rjlx8j2p50qn", "publishedVersionId": "cm9jaftyh0080rjlx8j2p50qn", "updatedAt": "2025-04-16T02:04:16.476Z", "createdAt": "2025-04-16T02:04:03.300Z" } ], "next": "/v1/content?limit=2&cursor=cm9jaftye007yrjlx3ghggigi", "previous": null } ``` # List content Source: https://docs.usertour.io/api-reference/content/list-content /api-reference-v2/openapi.json get /v2/projects/{projectId}/content # The content object Source: https://docs.usertour.io/api-reference/content/model The content object represents a container for different types of interactive content (flows, checklists, and launchers). It manages the versioning of your content, tracking both the draft version being edited and the published version currently in use. A unique identifier for the content object. The object type identifier. Always set to "content" to distinguish it from other API objects. The name of the content as defined in the Builder interface. The type of content. Supported values: * "flow" - Interactive step-by-step guides * "checklist" - Task-based checklists * "launcher" - Trigger-based content launchers The identifier of the current draft version being edited in the Builder. The associated [content version object](/api-reference/content-versions/model) for the draft version. Can be expanded using the `?expand=draft_version` query parameter. The identifier of the currently published version in the environment. The associated [content version object](/api-reference/content-versions/model) for the published version. Can be expanded using the `?expand=published_version` query parameter. The timestamp when the content was last updated, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). The timestamp when the content was created, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). ```json Example content object theme={null} { "id": "cm9f6vwed0002iejc4vg2zu3t", "object": "content", "name": "Product Onboarding Flow", "type": "flow", "editedVersionId": "cm9mkru2c000mlhmajx3l0146", "editedVersion": { "id": "cm9mkru2c000mlhmajx3l0146", "object": "contentVersion", "number": 14, "questions": [], "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-18T09:16:37.989Z" }, "publishedVersionId": "cm9mjsv7d001bvfe8qe8tgzv9", "publishedVersion": { "id": "cm9mjsv7d001bvfe8qe8tgzv9", "object": "contentVersion", "number": 13, "questions": [], "updatedAt": "2025-04-18T08:49:26.501Z", "createdAt": "2025-04-18T08:49:26.501Z" }, "updatedAt": "2025-04-18T09:16:37.989Z", "createdAt": "2025-04-13T05:13:29.797Z" } ``` # Publish a version Source: https://docs.usertour.io/api-reference/content/publish-a-version /api-reference-v2/openapi.json post /v2/projects/{projectId}/content/{id}/publish Set a version as the live version in an environment (idempotent). # Restore deleted content Source: https://docs.usertour.io/api-reference/content/restore-deleted-content /api-reference-v2/openapi.json post /v2/projects/{projectId}/content/{id}/restore Restore soft-deleted content (find it via GET /content?deleted=true). It returns as an unpublished draft with versions and history intact — publish again explicitly to go live. Idempotent on content that is not deleted. # Unpublish a version Source: https://docs.usertour.io/api-reference/content/unpublish-a-version /api-reference-v2/openapi.json post /v2/projects/{projectId}/content/{id}/unpublish Clear an environment's live version for the content. # Update content Source: https://docs.usertour.io/api-reference/content/update-content /api-reference-v2/openapi.json patch /v2/projects/{projectId}/content/{id} Update content metadata (name, buildUrl). # Create an environment Source: https://docs.usertour.io/api-reference/environments/create-an-environment /api-reference-v2/openapi.json post /v2/projects/{projectId}/environments Create an environment in the project. The first one is made primary. # Delete an environment Source: https://docs.usertour.io/api-reference/environments/delete-an-environment /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{id} Delete an environment. The primary / last environment cannot be deleted. # Get an environment Source: https://docs.usertour.io/api-reference/environments/get-an-environment /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{id} # List environments Source: https://docs.usertour.io/api-reference/environments/list-environments /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments # Update an environment Source: https://docs.usertour.io/api-reference/environments/update-an-environment /api-reference-v2/openapi.json patch /v2/projects/{projectId}/environments/{id} Rename an environment. # Errors Source: https://docs.usertour.io/api-reference/errors Troubleshoot problems with this comprehensive breakdown of all error codes. ## Error schema We use standard HTTP response codes for success and failure notifications, and our errors are further classified by type. ## Error responses Error responses contains an `error` object with the following properties: Machine-readable error code. This is the same as the error code shown in the error documentation. Human-readable explanation of what went wrong. This message is designed to be helpful for developers. URL to the Usertour API docs. This link will take you to the relevant documentation for the error. **Example error response:** ```json theme={null} { "error": { "code": "E1017", "message": "each value in orderBy must be one of the following values: createdAt, -createdAt, codeName, -codeName, displayName, -displayName", "doc_url": "https://docs.usertour.com" } } ``` ## Error codes ### `E1000` * **Status:** 403 * **Message:** Invalid API key provided * **Suggested action:** Check your API key and make sure it's correct. ### `E1010` * **Status:** 401 * **Message:** Missing API key * **Suggested action:** Add your API key to the request headers. ### `E1001` * **Status:** 404 * **Message:** User not found * **Suggested action:** Check the user ID and make sure it exists. ### `E1002` * **Status:** 404 * **Message:** Company not found * **Suggested action:** Check the company ID and make sure it exists. ### `E1003` * **Status:** 404 * **Message:** Company membership not found * **Suggested action:** Check the company membership ID and make sure it exists. ### `E1004` * **Status:** 404 * **Message:** Content not found * **Suggested action:** Check the content ID and make sure it exists. ### `E1005` * **Status:** 404 * **Message:** Content session not found * **Suggested action:** Check the content session ID and make sure it exists. ### `E1006` * **Status:** 400 * **Message:** Invalid limit parameter * **Suggested action:** Check the limit value and make sure it's valid. ### `E1007` * **Status:** 400 * **Message:** Invalid cursor parameter * **Suggested action:** Check the cursor value and make sure it's valid. ### `E1008` * **Status:** 400 * **Message:** Invalid previous cursor parameter * **Suggested action:** Check the previous cursor value and make sure it's valid. ### `E1009` * **Status:** 400 * **Message:** Invalid request * **Suggested action:** Check your request parameters and make sure they're valid. ### `E1013` * **Status:** 429 * **Message:** Too many requests * **Suggested action:** Reduce your request rate or upgrade your plan. ### `E1014` * **Status:** 503 * **Message:** Service unavailable * **Suggested action:** Try again later or contact support if the problem persists. ### `E1015` * **Status:** 400 * **Message:** Invalid scope parameter * **Suggested action:** Check the scope value and make sure it's valid. ### `E1016` * **Status:** 400 * **Message:** Invalid orderBy parameter * **Suggested action:** Check the orderBy value and make sure it's valid. ### `E1017` * **Status:** 400 * **Message:** Validation error * **Suggested action:** Check the value and make sure it's valid. # Create an event definition Source: https://docs.usertour.io/api-reference/event-definitions/create-an-event-definition /api-reference-v2/openapi.json post /v2/projects/{projectId}/event-definitions # Delete an event definition Source: https://docs.usertour.io/api-reference/event-definitions/delete-an-event-definition /api-reference-v2/openapi.json delete /v2/projects/{projectId}/event-definitions/{id} # Get an event definition Source: https://docs.usertour.io/api-reference/event-definitions/get-an-event-definition /api-reference-v2/openapi.json get /v2/projects/{projectId}/event-definitions/{id} # List event definitions Source: https://docs.usertour.io/api-reference/event-definitions/list Get /v1/event-definitions Retrieves a paginated list of event definitions in your account. Each definition includes its name, code, and metadata. This endpoint returns a paginated list of event definitions. Each definition contains its display name, code name, and creation timestamp. ## Query parameters Sort the results by one or more fields. Available fields: * `createdAt`: Creation timestamp * `codeName`: Technical name * `displayName`: Display name Examples: * Single field: `orderBy=createdAt` or `orderBy=-createdAt` (descending) * Multiple fields: `orderBy[]=-createdAt&orderBy[]=displayName` Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing an array of [event definition objects](/api-reference/event-definitions/model) in the `results` property. The response includes pagination information in the `next` and `previous` fields. ```bash Request theme={null} # List event definitions with pagination curl https://api.usertour.io/v1/event-definitions?limit=4&orderBy=createdAt \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "cm9e922p3001pmjao5vba8054", "object": "eventDefinition", "createdAt": "2025-04-12T13:26:30.952Z", "description": "", "displayName": "Flow Started", "codeName": "flow_started" }, { "id": "cm9e922p3001qmjaozg69mzk9", "object": "eventDefinition", "createdAt": "2025-04-12T13:26:30.952Z", "description": "", "displayName": "Flow Dismissed/Ended", "codeName": "flow_ended" }, { "id": "cm9e922p3001rmjaom1h20luk", "object": "eventDefinition", "createdAt": "2025-04-12T13:26:30.952Z", "description": "", "displayName": "Flow Step Seen", "codeName": "flow_step_seen" }, { "id": "cm9e922p4001smjaov92o3zq8", "object": "eventDefinition", "createdAt": "2025-04-12T13:26:30.952Z", "description": "", "displayName": "Flow Step Completed", "codeName": "flow_step_completed" } ], "next": "/v1/event-definitions?limit=4&orderBy=createdAt&cursor=cm9e922p4001smjaov92o3zq8", "previous": null } ``` # List event definitions Source: https://docs.usertour.io/api-reference/event-definitions/list-event-definitions /api-reference-v2/openapi.json get /v2/projects/{projectId}/event-definitions # The event definition object Source: https://docs.usertour.io/api-reference/event-definitions/model Event definitions are metadata objects that describe the structure and purpose of events in your application. They are automatically created when you track new events through the Usertour API or SDK. These definitions serve as a schema registry for your events, ensuring consistency in event tracking across your application. While you can't directly manage event definitions through the API, they are automatically maintained as you track events. A unique identifier for the event definition. The object type identifier. Always set to "eventDefinition" to distinguish it from other API objects. The timestamp when the event definition was first created, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). A technical description of the event's purpose and context. This field is editable in the Usertour Dashboard and helps maintain documentation of your event tracking implementation. The human-readable identifier for the event, used in the Usertour Dashboard and analytics interfaces. This can be modified to better reflect your event naming conventions. The immutable identifier used in your code when tracking events. This value is used in the `name` field of event objects and cannot be changed after creation to maintain data consistency. ```json Example event definition object theme={null} { "id": "cm9e922p3001pmjao5vba8054", "object": "eventDefinition", "createdAt": "2025-04-12T13:26:30.952Z", "description": "Triggered when a user starts a flow", "displayName": "Flow Started", "codeName": "flow_started" } ``` # Update an event definition Source: https://docs.usertour.io/api-reference/event-definitions/update-an-event-definition /api-reference-v2/openapi.json patch /v2/projects/{projectId}/event-definitions/{id} # Track an event Source: https://docs.usertour.io/api-reference/events/track-an-event /api-reference-v2/openapi.json post /v2/projects/{projectId}/environments/{environmentId}/events Records a behavior event for a user. Unseen users are created; an unknown event name registers a definition on first use; built-in Usertour event names are refused. # Expanding objects Source: https://docs.usertour.io/api-reference/expanding-objects Learn how to expand related objects in API responses using the ```expand``` query parameter. ## Overview The API supports expanding related objects in responses using the `expand` query parameter. This allows you to retrieve complete objects instead of just their IDs. For example, when fetching an event, you can expand the associated user object to get the full user details instead of just the `userId`. ## Basic Usage ### Single Object Expansion For one-to-one relationships, use the `expand` parameter to include the full object. For example, adding `?expand=user` to a request will include the complete user object in the response. ### List Expansion For one-to-many relationships, the API will return an array of expanded objects. By default, these fields are `null` (not expanded). For example, adding `?expand=memberships` will include an array of all the user's memberships. ## Advanced Usage ### Nested Expansion You can expand nested relationships by using dot notation (`.`). For example, `?expand=memberships.company` will expand both the memberships and their associated companies. ### Multiple Expansions To expand multiple objects simultaneously, use the array syntax with multiple paths: ```bash theme={null} ?expand[]=user&expand[]=company ``` ## Supported Endpoints The `expand` parameter can be used on any endpoint that returns expandable fields, including: * List endpoints * Create endpoints * Update endpoints ## Example ### Request ```bash theme={null} curl https://api.usertour.io/v1/users/1744521292871a?expand[]=memberships&expand[]=companies&expand[]=memberships.company \ -H 'Authorization: Bearer ' ``` ### Response ```json theme={null} { "id": "1744521292871a", "object": "user", "attributes": {}, "createdAt": "2025-04-27T13:39:47.024Z", "companies": [ { "id": "1744521292871a", "object": "company", "attributes": {}, "createdAt": "2025-04-27T13:39:47.024Z" } ], "memberships": [ { "id": "cma9nhdis0002108kpmr9vfrl", "object": "companyMembership", "attributes": { "role": "admin" }, "createdAt": "2025-05-04T12:51:10.881Z", "companyId": "cm9zp4wyh00116pt4o5v5vg5t", "userId": "cm9zp4wyb000z6pt484wnbzs7", "company": { "id": "1744521292871a", "object": "company", "attributes": {}, "createdAt": "2025-04-27T13:39:47.024Z" } } ] } ``` # Introduction Source: https://docs.usertour.io/api-reference/introduction Understand general concepts, response codes, and authentication strategies. This is the **legacy v1 API**. New integrations should use the [v2 API](/api-reference-v2/introduction) — it adds content authoring, publishing, and an MCP endpoint. v1 remains available for existing integrations. ## Base URL The Usertour API is built on **REST** principles. We enforce **HTTPS** in every request to improve data security, integrity, and privacy. The API does not support **HTTP**. All requests contain the following base URL: ``` https://api.usertour.io ``` ## Authentication To authenticate you need to add an Authorization header with the contents of the header being `Bearer ak_123456789` where `ak_123456789` is your [API Key](https://app.usertour.io/project/1/settings/api). ``` Authorization: Bearer ak_123456789 ``` ## Response codes Usertour uses standard HTTP codes to indicate the success or failure of your requests. In general, `2xx` HTTP codes correspond to success, `4xx` codes are for user-related failures, and `5xx` codes are for infrastructure issues. | Status | Description | | ------ | ----------------------------------------- | | `200` | Successful request. | | `400` | Check that the parameters were correct. | | `401` | The API key used was missing. | | `403` | The API key used was invalid. | | `404` | The resource was not found. | | `429` | The rate limit was exceeded. | | `5xx` | Indicates an error with Usertour servers. | Check [**Error Codes**](/api-reference/errors) for a comprehensive breakdown of all possible API errors. ## Rate limit The API implements two protection mechanisms to ensure service stability: 1. **Request Rate Limiter** * Enforces per-account request limits per minute * Rate limits vary based on your subscription plan (see [Pricing](https://www.usertour.io/pricing)) * Exceeding the limit returns HTTP 429 (Too Many Requests) 2. **Concurrency Limiter** * Controls simultaneous active requests * Implements request queuing when at high capacity * Queue timeout: 15 seconds * Returns HTTP 503 (Service Unavailable) if queued too long * Note: This is a rare occurrence as we maintain sufficient capacity to minimize queuing For optimal integration, implement proper error handling for both 429 and 503 responses. Consider implementing retry logic with exponential backoff when receiving these status codes. # Introspect the token Source: https://docs.usertour.io/api-reference/me/introspect-the-token /api-reference-v2/openapi.json get /v2/me Returns the token's name and the projects and environments it may act on — the credential-test and picker-population call for integration platforms. # Ordering Source: https://docs.usertour.io/api-reference/ordering Some top-level API resources that support listing can be ordered by one or more fields using the orderBy query parameter. Each resource will list which fields it can be ordered by. ## Query parameters Specifies the field(s) to sort the results by. You can provide either a single field or an array of fields. When multiple fields are specified, the results are sorted by the first field first, then by the second field, and so on. By default, fields are sorted in ascending order. To sort in descending order, prefix the field name with a hyphen (-). ## Examples ### Order by a single field ```bash theme={null} GET /users?orderBy=name ``` This will sort the results by the name field in ascending order. ### Order by multiple fields ```bash theme={null} GET /users?orderBy[]=attributes.name&orderBy[]=createdAt ``` This will first sort by the name attribute, and then by the creation date, both in ascending order. ### Use descending order ```bash theme={null} GET /users?orderBy=-createdAt ``` This will sort the results by the creation date in descending order (newest first). # Pagination Source: https://docs.usertour.io/api-reference/pagination All top-level API resources that support listing use the same format for pagination and the same response object. ## Query parameters Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. ## The list object All list endpoints return a standardized response object with the following structure: An array containing the requested resource objects (e.g., users, companies). The number of items will not exceed the specified limit. The URL to fetch the next page of results. This field is always present, as it allows clients to know how to fetch the next page if new items become available later. The URL to fetch the previous page of results. This field is null when you're on the first page or when there is no previous page available. ```json theme={null} { "results": [ { "id": "1744521292871", "object": "user", ... }, { "id": "1744521298100", "object": "user", ... } ... ], "next": "/v1/users?limit=2&cursor=cm9f6xsjr0014iejcuc80dy3q", "previous": null } ``` # Add a segment member Source: https://docs.usertour.io/api-reference/segments/add-a-segment-member /api-reference-v2/openapi.json put /v2/projects/{projectId}/environments/{environmentId}/segments/{id}/members/{externalId} Add a user or company (per the segment's bizType) to a manual segment. # Create a segment Source: https://docs.usertour.io/api-reference/segments/create-a-segment /api-reference-v2/openapi.json post /v2/projects/{projectId}/segments # Delete a segment Source: https://docs.usertour.io/api-reference/segments/delete-a-segment /api-reference-v2/openapi.json delete /v2/projects/{projectId}/segments/{id} # Get a segment Source: https://docs.usertour.io/api-reference/segments/get-a-segment /api-reference-v2/openapi.json get /v2/projects/{projectId}/segments/{id} # List segments Source: https://docs.usertour.io/api-reference/segments/list-segments /api-reference-v2/openapi.json get /v2/projects/{projectId}/segments # Remove a segment member Source: https://docs.usertour.io/api-reference/segments/remove-a-segment-member /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{environmentId}/segments/{id}/members/{externalId} Remove a user or company (per the segment's bizType) from a manual segment. # Update a segment Source: https://docs.usertour.io/api-reference/segments/update-a-segment /api-reference-v2/openapi.json patch /v2/projects/{projectId}/segments/{id} # Delete a session Source: https://docs.usertour.io/api-reference/sessions/delete-a-session /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{environmentId}/sessions/{id} PERMANENT, with three distinct consequences: the session record is gone (no restore exists); its recorded answers vanish from question analytics irreversibly — deleting "test" sessions rewrites real response counts; and attribute values the session wrote onto the user via bindAttribute REMAIN on the user (clearing those is a separate user-attribute update, or the profile will contradict the survey analytics). # End a session Source: https://docs.usertour.io/api-reference/sessions/end-a-session /api-reference-v2/openapi.json post /v2/projects/{projectId}/environments/{environmentId}/sessions/{id}/end End an in-progress session. Idempotent: a session already in its terminal state is returned as-is. Tracker sessions have no end semantics and refuse with E1017. # Get a session Source: https://docs.usertour.io/api-reference/sessions/get-a-session /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/sessions/{id} # List sessions Source: https://docs.usertour.io/api-reference/sessions/list-sessions /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/sessions Sessions in this environment. Filter by contentId / userId. # Create a theme Source: https://docs.usertour.io/api-reference/themes/create-a-theme /api-reference-v2/openapi.json post /v2/projects/{projectId}/themes # Delete a theme Source: https://docs.usertour.io/api-reference/themes/delete-a-theme /api-reference-v2/openapi.json delete /v2/projects/{projectId}/themes/{id} Rejected for the default / system theme, and while any live or draft version still uses the theme (409 E1031 — switch that content to another theme first). Historical versions do not block deletion. # Duplicate a theme Source: https://docs.usertour.io/api-reference/themes/duplicate-a-theme /api-reference-v2/openapi.json post /v2/projects/{projectId}/themes/{id}/duplicate Copy a theme (settings + variations verbatim) into a fresh non-default theme. System themes may be duplicated (the natural "derive from Standard Light" flow). # Get a theme Source: https://docs.usertour.io/api-reference/themes/get-a-theme /api-reference-v2/openapi.json get /v2/projects/{projectId}/themes/{id} # List themes Source: https://docs.usertour.io/api-reference/themes/list-themes /api-reference-v2/openapi.json get /v2/projects/{projectId}/themes # Update a theme Source: https://docs.usertour.io/api-reference/themes/update-a-theme /api-reference-v2/openapi.json patch /v2/projects/{projectId}/themes/{id} # Upsert a user Source: https://docs.usertour.io/api-reference/users/create Post /v1/users This endpoint allows you to create a new user or update an existing one. The operation is idempotent - if a user with the specified ID already exists, the request will update the user's attributes. If the user doesn't exist, a new user will be created. ## Body Parameters Unique identifier for the user. This should match the user's ID in your system. A map of user attributes to update. You can include any custom attributes. Existing attributes not included in the request will remain unchanged. See [Attributes](/api-reference/attributes) for details. An array of company objects to associate with the user. Each company object must contain an ID and can include additional attributes. This is the recommended approach when you only need to establish company associations without additional relationship attributes. Unique identifier for the company. Must match the company's ID in your system. A map of company-specific attributes. You can include any custom attributes to describe the company. An array of membership objects that define the relationship between a user and a company. Use this when you need to specify attributes that describe the user's relationship with each company. Each membership object must include a company object with at least its ID. A map of attributes that describe the user's relationship with the company. You can include any custom attributes to define the nature of this relationship. The company object associated with this membership. Unique identifier for the company. Must match the company's ID in your system. A map of company-specific attributes. You can include any custom attributes to describe the company. Only one of companies and memberships can be set. ```bash Basic Request theme={null} curl https://api.usertour.io/v1/users \ -XPOST \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' \ -d '{ "id": "usr_123456789", "attributes": { "name": "John Doe", "email": "john.doe@example.com", "signed_up_at": "2024-03-20T08:30:00.000Z" } }' ``` ```bash With Companies theme={null} curl https://api.usertour.io/v1/users \ -XPOST \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' \ -d '{ "id": "usr_123456789", "attributes": { "name": "John Doe", "email": "john.doe@example.com" }, "companies": [ { "id": "comp_987654321", "attributes": { "name": "Acme Corporation", "plan": "enterprise", "industry": "technology" } } ] }' ``` ```bash With Memberships theme={null} curl https://api.usertour.io/v1/users \ -XPOST \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' \ -d '{ "id": "usr_123456789", "attributes": { "name": "John Doe", "email": "john.doe@example.com" }, "memberships": [ { "attributes": { "role": "admin", "department": "engineering", "join_date": "2024-01-01T00:00:00.000Z" }, "company": { "id": "comp_987654321", "attributes": { "name": "Acme Corporation", "plan": "enterprise", "industry": "technology" } } } ] }' ``` ```json Response theme={null} { "id": "usr_123456789", "object": "user", "attributes": { "name": "John Doe", "email": "john.doe@example.com" }, "created_at": "2024-03-20T08:30:00.000Z" } ``` # Create or update a user Source: https://docs.usertour.io/api-reference/users/create-or-update-a-user /api-reference-v2/openapi.json put /v2/projects/{projectId}/environments/{environmentId}/users/{id} # Delete a user Source: https://docs.usertour.io/api-reference/users/delete Delete /v1/users/:id ## Path Parameters Unique identifier for the user. This should match the user's ID in your system. ## Response Unique identifier for the user. This should match the user's ID in your system. Represents the object's type. Always "user". Indicates whether the user was successfully deleted. Always true when the request succeeds. ```bash Request theme={null} curl https://api.usertour.io/v1/users/usr_123456789 \ -XDELETE \ -H 'Authorization: Bearer ak_123456789' ``` ```json Response theme={null} { "id": "usr_123456789", "object": "user", "deleted": true } ``` # Delete a user Source: https://docs.usertour.io/api-reference/users/delete-a-user /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{environmentId}/users/{id} DESTRUCTIVE and permanent — removes the user AND everything hanging off them in this environment: attributes, company memberships, segment memberships, question answers and event history. There is no restore; re-identifying the same external id later starts a brand-new user. # Get a user Source: https://docs.usertour.io/api-reference/users/get Get /v1/users/:id Retrieves a specific user by their ID, including all their attributes and relationships. ## Path Parameters The unique identifier of the user to retrieve. This ID should match the user's identifier in your system. ## Query Parameters See [Expanding objects](/api-reference/expanding-objects). Available options: * `memberships`: Include basic membership data for each user * `memberships.company`: Include both membership and associated company details * `companies`: Include all companies associated with the user * You can request multiple expansions: `expand[]=memberships&expand[]=companies` ## Response Returns a [user object](/api-reference/users/model) if found. Returns a 404 Not Found error if the user doesn't exist. ```bash Request theme={null} # Get a user with their memberships, company details, and membership-company relationships curl https://api.usertour.io/v1/users/usr_123456789?expand[]=memberships&expand[]=memberships.company&expand[]=companies \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "id": "usr_123456789", "object": "user", "attributes": { "name": "John Doe", "email": "john.doe@example.com", "role": "admin", "department": "engineering", "signed_up_at": "2024-03-20T08:30:00.000Z" }, "createdAt": "2024-03-20T08:30:00.000Z", "companies": [ { "id": "comp_123456789", "object": "company", "attributes": { "name": "Acme Corp", "domain": "acme.com" }, "createdAt": "2024-03-20T08:30:00.000Z" } ], "memberships": [ { "id": "cma9nhdis0002108kpmr9vfrl", "object": "companyMembership", "attributes": { "role": "admin", "department": "engineering" }, "createdAt": "2024-03-20T08:30:00.000Z", "companyId": "comp_123456789", "userId": "usr_123456789", "company": { "id": "comp_123456789", "object": "company", "attributes": { "name": "Acme Corp", "domain": "acme.com" }, "createdAt": "2024-03-20T08:30:00.000Z" } } ] } ``` # Get a user Source: https://docs.usertour.io/api-reference/users/get-a-user /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/users/{id} # List users Source: https://docs.usertour.io/api-reference/users/list Get /v1/users Retrieve a paginated list of users. You can filter the results using various query parameters. ## Query parameters Filter users by their email address. The search is case-insensitive and must match the exact email value stored in the user's attributes. Filter users by segment membership. You can find the segment ID in the Usertour UI by clicking the three-dot menu in the top right corner of the segment page. Filter users by company membership. Only returns users who are members of the specified company. Specifies the maximum number of items to return in a single response. The value must be between 1 and 100. If not specified, defaults to 20 items per page. Specifies the starting point for the next page of results. The response will include items that come after (but not including) the object with this ID. To get the next page, use the ID of the last item from your previous request. The easiest way is to use the [list object's](#the-list-object) cursor field. If not provided, the API will return items from the beginning of the list. See [Expanding objects](/api-reference/expanding-objects). Available options: * `memberships`: Include basic membership data for each user * `memberships.company`: Include both membership and associated company details * `companies`: Include all companies associated with the user * You can request multiple expansions: `expand[]=memberships&expand[]=companies` ## Response Returns a [list object](/api-reference/pagination#the-list-object) containing an array of [user objects](/api-reference/users/model) in the `results` property. The response includes pagination information in the `next` and `previous` fields. ```bash Request theme={null} curl https://api.usertour.io/v1/users \ -H 'Authorization: Bearer ak_123456789' \ -H 'Content-Type: application/json' ``` ```json Response theme={null} { "results": [ { "id": "usr_123456789", "object": "user", "attributes": { "name": "John Doe", "email": "john.doe@example.com", "role": "admin", "department": "engineering", "last_login_at": "2024-03-20T08:30:00.000Z" }, "createdAt": "2024-03-20T08:30:00.000Z", "companies": null, "memberships": null }, { "id": "usr_987654321", "object": "user", "attributes": { "name": "Jane Smith", "email": "jane.smith@example.com", "role": "user", "department": "marketing", "last_login_at": "2024-03-19T15:45:00.000Z" }, "createdAt": "2024-03-19T15:45:00.000Z", "companies": null, "memberships": null } ], "next": "/v1/users?limit=2&cursor=usr_987654321", "previous": null } ``` # List users Source: https://docs.usertour.io/api-reference/users/list-users /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/users # The user object Source: https://docs.usertour.io/api-reference/users/model A unique identifier for the user. This ID should match the user's identifier in your system. The object type identifier. Always set to "user" to distinguish it from other API objects. A collection of user attributes. You can include any custom attributes to describe the user. See [Attributes](/api-reference/attributes) for details. The timestamp when the user was created in the system, in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Note: This is not the user's signup time in your application - use a custom attribute like `signed_up_at` for that. A list of [company objects](/api-reference/companies/model) that the user belongs to. This field is useful when you only need company information without membership details. If you need membership attributes (like roles or permissions), use the `memberships` field instead. Can be expanded using `?expand=companies`. A list of [company membership objects](/api-reference/company-memberships/model) representing the user's company memberships. Each membership includes: * Company-specific attributes (e.g., role, access level) * Reference to the associated [company object](/api-reference/companies/model) Available expansion options: * `?expand=memberships`: Include basic membership data * `?expand=memberships.company`: Include both membership and company details ```json Example user object theme={null} { "id": "1744521292871a", "object": "user", "attributes": { "name": "John Doe", "email": "john@example.com", "role": "admin" }, "createdAt": "2025-04-27T13:39:47.024Z", "companies": [ { "id": "1744521292871a", "object": "company", "attributes": { "name": "Acme Corp", "domain": "acme.com" }, "createdAt": "2025-04-27T13:39:47.024Z" } ], "memberships": [ { "id": "cma9nhdis0002108kpmr9vfrl", "object": "companyMembership", "attributes": { "role": "admin", "department": "engineering" }, "createdAt": "2025-05-04T12:51:10.881Z", "companyId": "cm9zp4wyh00116pt4o5v5vg5t", "userId": "cm9zp4wyb000z6pt484wnbzs7", "company": { "id": "1744521292871a", "object": "company", "attributes": { "name": "Acme Corp", "domain": "acme.com" }, "createdAt": "2025-04-27T13:39:47.024Z" } } ] } ``` # Create a webhook Source: https://docs.usertour.io/api-reference/webhooks/create-a-webhook /api-reference-v2/openapi.json post /v2/projects/{projectId}/environments/{environmentId}/webhooks On Usertour Cloud, webhooks need a paid plan (Starter or above) — a Hobby project gets 403 E0043. Self-hosted instances are never gated. The same applies to update, rotate-secret and delivery; reads and delete stay available on any plan. # Delete a webhook Source: https://docs.usertour.io/api-reference/webhooks/delete-a-webhook /api-reference-v2/openapi.json delete /v2/projects/{projectId}/environments/{environmentId}/webhooks/{id} # Get a webhook (includes the signing secret for webhook:manage tokens) Source: https://docs.usertour.io/api-reference/webhooks/get-a-webhook-includes-the-signing-secret-for-webhook:manage-tokens /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/webhooks/{id} The signing secret is the ability to forge signed deliveries, so it rides only on tokens holding webhook:manage — a read-only token gets every other field. # List webhooks Source: https://docs.usertour.io/api-reference/webhooks/list-webhooks /api-reference-v2/openapi.json get /v2/projects/{projectId}/environments/{environmentId}/webhooks # Rotate the signing secret Source: https://docs.usertour.io/api-reference/webhooks/rotate-the-signing-secret /api-reference-v2/openapi.json post /v2/projects/{projectId}/environments/{environmentId}/webhooks/{id}/rotate-secret # Update a webhook Source: https://docs.usertour.io/api-reference/webhooks/update-a-webhook /api-reference-v2/openapi.json patch /v2/projects/{projectId}/environments/{environmentId}/webhooks/{id} # Build Your Onboarding with AI Source: https://docs.usertour.io/build-onboarding-with-ai Watch an AI assistant build production-ready onboarding — a flow, a checklist, a survey, an announcement and a resource center — in a real app, then follow along with the exact prompts. Usertour is open-source user onboarding with an [MCP server](/api-reference-v2/mcp) built in: connect an AI assistant like Claude Code, Cursor or Codex, describe the experience you want, and it authors, themes and publishes the content directly in your project — matched to your app's design, verified in your running app. The video below does exactly that against a real app ([shadcn-admin](https://github.com/satnaing/shadcn-admin)): four prompts, and after each one — the result live in the app, then the content and data in the Usertour dashboard.