> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usertour.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Duplicate a theme

> Copy a theme (settings + variations verbatim) into a fresh non-default theme. System themes may be duplicated (the natural "derive from Standard Light" flow).



## OpenAPI

````yaml /api-reference-v2/openapi.json post /v2/projects/{projectId}/themes/{id}/duplicate
openapi: 3.0.0
info:
  title: Usertour API v2
  description: >-
    Project-scoped v2 API. Authenticate with a personal API token — an opaque
    `utp_...` string (NOT a JWT: do not try to decode it), created in the
    Usertour app under Settings → API, sent as `Authorization: Bearer utp_...`.
  version: '2.0'
  contact: {}
servers:
  - url: https://api.usertour.io
security: []
tags: []
paths:
  /v2/projects/{projectId}/themes/{id}/duplicate:
    post:
      tags:
        - Themes
      summary: Duplicate a theme
      description: >-
        Copy a theme (settings + variations verbatim) into a fresh non-default
        theme. System themes may be duplicated (the natural "derive from
        Standard Light" flow).
      operationId: ApiThemesController_duplicate
      parameters:
        - name: projectId
          required: true
          in: path
          description: Project ID
          schema:
            type: string
        - name: id
          required: true
          in: path
          description: Theme ID (the source)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DuplicateThemeBodyDto'
      responses:
        '201':
          description: Theme duplicated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThemeDto'
        '400':
          description: >-
            Invalid request — E1017 validation (may carry `issues`; an invalid
            orderBy/limit is also E1017), E1015 invalid scope, E0003 invalid
            against current domain state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseDto'
        '401':
          description: Missing or expired API key — E1010, E1020.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseDto'
        '403':
          description: >-
            Refused — E1000 invalid key, E1011 project not in token scope, E1012
            insufficient scope, E1029 environment not in token scope, E1032
            environment creation needs a token without env-targeted capabilities
            (its allowlist cannot cover a not-yet-existing environment).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseDto'
        '404':
          description: Theme not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseDto'
        '429':
          description: >-
            Rate limit exceeded — E1013. The limit follows the project's plan
            (100/500/1000/3000 requests per minute); unknown credentials share a
            per-IP bucket. Every response also carries X-RateLimit-Limit /
            -Remaining / -Reset for pacing; a 429 adds the standard Retry-After
            header (seconds to back off).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseDto'
      security:
        - bearer: []
components:
  schemas:
    DuplicateThemeBodyDto:
      type: object
      properties:
        name:
          description: Name for the copy (defaults to the source name).
          type: string
          minLength: 1
      additionalProperties: false
    ThemeDto:
      type: object
      properties:
        id:
          type: string
        object:
          type: string
          enum:
            - theme
        name:
          type: string
        isDefault:
          type: boolean
        isSystem:
          type: boolean
          description: >-
            Built-in theme: read-only — it cannot be updated or deleted
            (duplicating it into your own theme is the way to start from it). It
            CAN be made the project default.
        updatedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        variationCount:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: >-
            How many conditional variations this theme carries (always present,
            no expand needed). Check it BEFORE pointing content at another
            theme: variations do NOT travel with the content, so moving from a
            theme with variations to one with 0 silently drops the conditional
            styling (e.g. dark mode) for every user those conditions targeted —
            nothing errors. Read the variations themselves with expand:
            ["variations"].
        settings:
          description: >-
            Stored style INTENT — the same shape as the create/update `settings`
            body (that write schema is the field dictionary); Auto-capable
            colors read back as the literal "Auto".
          type: object
          additionalProperties: {}
        resolvedSettings:
          description: >-
            Read-only render resolution of `settings` (the shared derivation the
            SDK runs). Two transformations: every "Auto" is replaced by the
            concrete derived color, and `font.fontFamily` is rewritten to the
            real font stack ("System font" → the system stack, "Custom font" →
            the `customFontFamily` value, named fonts gain a sans-serif
            fallback). Same field set as `settings` (stored settings are
            grounded complete at write) — differences between the two are
            exactly these derivations, so diff against the `settings` you wrote,
            not this, to find your edits. Request with expand:
            ["resolvedSettings"]. Not writable — author intent in `settings`.
          type: object
          additionalProperties: {}
        variations:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              conditions:
                type: array
                items:
                  $ref: '#/components/schemas/ThemeDto__schema0'
              settings:
                type: object
                additionalProperties: {}
            required:
              - id
              - name
              - conditions
              - settings
      required:
        - id
        - object
        - name
        - isDefault
        - isSystem
        - updatedAt
        - createdAt
        - variationCount
    ErrorResponseDto:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: >-
                Stable machine-readable code (e.g. E1017). Match on this, never
                on `message`.
            message:
              type: string
              description: Human-readable summary. Wording may change between releases.
            issues:
              description: >-
                Validation errors (E1017) may carry one entry per problem so
                every field can be fixed in a single round-trip. Absent on other
                errors.
              type: array
              items:
                type: object
                properties:
                  rule:
                    type: string
                    description: >-
                      Which validation layer rejected it: schema |
                      reactive_condition | action_not_allowed | step_shape |
                      reference_target | auto_start | media_url. New values may
                      be added; treat an unknown value as a generic validation
                      failure.
                  message:
                    type: string
                  path:
                    description: >-
                      Path into the request body (e.g.
                      `steps[0].triggers[0].when[1]`).
                    type: string
                required:
                  - rule
                  - message
            doc_url:
              type: string
              description: Base URL of the API documentation.
      required:
        - error
    ThemeDto__schema0:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              enum:
                - group
            match:
              type: string
              enum:
                - all
                - any
            conditions:
              minItems: 1
              type: array
              items:
                $ref: '#/components/schemas/ThemeDto__schema0'
              description: >-
                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.
          required:
            - type
            - match
            - conditions
        - type: object
          properties:
            type:
              type: string
              enum:
                - attribute
            scope:
              type: string
              enum:
                - user
                - company
                - companyMembership
              description: >-
                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:
              type: string
            op:
              type: string
              enum:
                - 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
              description: >-
                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:
              description: >-
                The comparison value (string / number-as-string / date). Omit
                for any/empty/true/false.
              type: string
            value2:
              description: >-
                Upper bound for the `between` operator (`value` is the lower
                bound).
              type: string
            values:
              description: Values for the List operators (includes_any / includes_all / …).
              type: array
              items:
                type: string
          required:
            - type
            - scope
            - attribute
            - op
        - type: object
          properties:
            type:
              type: string
              enum:
                - segment
            segment:
              type: string
            in:
              type: boolean
          required:
            - type
            - segment
            - in
        - type: object
          properties:
            type:
              type: string
              enum:
                - current_url
            includes:
              type: array
              items:
                type: string
              description: >-
                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:
              description: >-
                URL patterns to exclude (same syntax as includes); excludes win
                over includes.
              type: array
              items:
                type: string
          required:
            - type
            - includes
        - type: object
          properties:
            type:
              type: string
              enum:
                - element
            target:
              type: object
              properties:
                selector:
                  type: string
                  minLength: 1
                  description: >-
                    A stable CSS selector for the element. The runtime targets
                    the FIRST match — so either make the selector unique, or
                    pair a stable non-unique selector with `nth` to pick the
                    intended match. A non-unique selector with no `nth` targets
                    the first (often wrong) element and the tooltip silently
                    won't render.
                text:
                  description: >-
                    Optional refinement of `selector`. Requires the targeted
                    element's visible text to equal this (exact match, after
                    trim) — use it to pin a specific content/state (e.g. a
                    stable id whose displayed text you want to match: id +
                    text). It refines the element chosen by `selector`/`nth`; on
                    its own it does NOT search among multiple matches, so
                    disambiguate a non-unique selector with `nth`, not `text`
                    alone.
                  type: string
                nth:
                  description: >-
                    Optional refinement of `selector`. 0-based index to pick
                    which match when the selector isn't unique — e.g. a stable
                    selector matching 3 elements + `nth: 1` targets the 2nd,
                    giving a unique result. Matches are taken in document order
                    (as they appear in the page HTML). Range 0–4 — only the
                    first 5 matches are addressable; a larger value is rejected.
                  type: integer
                  minimum: 0
                  maximum: 4
              required:
                - selector
            state:
              type: string
              enum:
                - present
                - hidden
                - disabled
                - enabled
                - clicked
                - unclicked
              description: >-
                `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 `<p>` 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.
          required:
            - type
            - state
        - type: object
          properties:
            type:
              type: string
              enum:
                - content_state
            content:
              type: string
              description: >-
                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:
              type: string
              enum:
                - seen
                - unseen
                - completed
                - uncompleted
                - active
                - inactive
              description: >-
                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.
          required:
            - type
            - content
            - state
        - type: object
          properties:
            type:
              type: string
              enum:
                - event
            event:
              type: string
            count:
              description: >-
                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".
              type: object
              properties:
                op:
                  type: string
                  enum:
                    - at_least
                    - at_most
                    - exactly
                    - between
                'n':
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                n2:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
              required:
                - op
                - 'n'
            within:
              description: >-
                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.
              type: object
              properties:
                op:
                  type: string
                  enum:
                    - in_the_last
                    - more_than
                    - between
                    - any_time
                value:
                  type: number
                value2:
                  type: number
                unit:
                  type: string
                  enum:
                    - seconds
                    - minutes
                    - hours
                    - days
              required:
                - op
            scope:
              description: >-
                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.
              type: string
              enum:
                - current_user
                - current_user_in_company
                - any_user_in_company
            where:
              type: array
              items:
                $ref: '#/components/schemas/ThemeDto__schema1'
          required:
            - type
            - event
        - type: object
          properties:
            type:
              type: string
              enum:
                - text_input
            target:
              type: object
              properties:
                selector:
                  type: string
                  minLength: 1
                  description: >-
                    A stable CSS selector for the element. The runtime targets
                    the FIRST match — so either make the selector unique, or
                    pair a stable non-unique selector with `nth` to pick the
                    intended match. A non-unique selector with no `nth` targets
                    the first (often wrong) element and the tooltip silently
                    won't render.
                text:
                  description: >-
                    Optional refinement of `selector`. Requires the targeted
                    element's visible text to equal this (exact match, after
                    trim) — use it to pin a specific content/state (e.g. a
                    stable id whose displayed text you want to match: id +
                    text). It refines the element chosen by `selector`/`nth`; on
                    its own it does NOT search among multiple matches, so
                    disambiguate a non-unique selector with `nth`, not `text`
                    alone.
                  type: string
                nth:
                  description: >-
                    Optional refinement of `selector`. 0-based index to pick
                    which match when the selector isn't unique — e.g. a stable
                    selector matching 3 elements + `nth: 1` targets the 2nd,
                    giving a unique result. Matches are taken in document order
                    (as they appear in the page HTML). Range 0–4 — only the
                    first 5 matches are addressable; a larger value is rejected.
                  type: integer
                  minimum: 0
                  maximum: 4
              required:
                - selector
            op:
              type: string
              enum:
                - is
                - not
                - contains
                - not_contains
                - starts_with
                - ends_with
                - match
                - unmatch
                - any
                - empty
            value:
              type: string
          required:
            - type
            - op
        - type: object
          properties:
            type:
              type: string
              enum:
                - text_filled
            target:
              type: object
              properties:
                selector:
                  type: string
                  minLength: 1
                  description: >-
                    A stable CSS selector for the element. The runtime targets
                    the FIRST match — so either make the selector unique, or
                    pair a stable non-unique selector with `nth` to pick the
                    intended match. A non-unique selector with no `nth` targets
                    the first (often wrong) element and the tooltip silently
                    won't render.
                text:
                  description: >-
                    Optional refinement of `selector`. Requires the targeted
                    element's visible text to equal this (exact match, after
                    trim) — use it to pin a specific content/state (e.g. a
                    stable id whose displayed text you want to match: id +
                    text). It refines the element chosen by `selector`/`nth`; on
                    its own it does NOT search among multiple matches, so
                    disambiguate a non-unique selector with `nth`, not `text`
                    alone.
                  type: string
                nth:
                  description: >-
                    Optional refinement of `selector`. 0-based index to pick
                    which match when the selector isn't unique — e.g. a stable
                    selector matching 3 elements + `nth: 1` targets the 2nd,
                    giving a unique result. Matches are taken in document order
                    (as they appear in the page HTML). Range 0–4 — only the
                    first 5 matches are addressable; a larger value is rejected.
                  type: integer
                  minimum: 0
                  maximum: 4
              required:
                - selector
          required:
            - type
        - type: object
          properties:
            type:
              type: string
              enum:
                - time_window
            start:
              type: string
              description: >-
                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:
              description: >-
                Window end (ISO datetime). Omit for an open-ended "from start
                onwards" window.
              type: string
          required:
            - type
            - start
        - type: object
          properties:
            type:
              type: string
              enum:
                - unsupported
            note:
              type: string
          required:
            - type
          description: >-
            Read-side placeholder for a stored condition this API cannot express
            (`note` says what it stands for — usually a DEAD condition the
            runtime never matches: a deleted attribute/event, an end-only time
            window). It cannot be written back (the placeholder carries no data
            to preserve): echoing it is rejected. Either remove it from the list
            you write — an explicit choice that DELETES the stored condition;
            mind that 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.
    ThemeDto__schema1:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              enum:
                - event_attribute
            attribute:
              type: string
            op:
              type: string
              enum:
                - 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
            value:
              type: string
            value2:
              type: string
            values:
              type: array
              items:
                type: string
          required:
            - type
            - attribute
            - op
        - type: object
          properties:
            type:
              type: string
              enum:
                - group
            match:
              type: string
              enum:
                - all
                - any
            conditions:
              minItems: 1
              type: array
              items:
                $ref: '#/components/schemas/ThemeDto__schema1'
              description: >-
                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.
          required:
            - type
            - match
            - conditions
  securitySchemes:
    bearer:
      scheme: bearer
      bearerFormat: utp_... personal API token (opaque)
      type: http

````