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

# Create Composition

> Creates a new composition.

To make the composition for one of your managed users, send `managedUserId` or `managedUserExternalId`.
The composition stays in your account and is yours to edit; the tag lets you list one user's compositions
with `getAllCompositions`. The managed user must be yours and active: one that is not yours, or does not
exist, is answered with 404 and `MANAGED_USER_NOT_FOUND`, and one that is switched off with 409 and
`MANAGED_USER_INACTIVE`. The tag is set here and does not change afterwards. A copy made with
`duplicateComposition` keeps the tag of the original.

An editor session with the `creator` surface may call this to start a composition of a widget it built:
it sends only `name`, `widgetId` (a widget made for the session's managed user) and `content`, and the
composition is always made for that managed user.

Send `databaseSchema` to give the composition tables of its own for what visitors send it (the shape, the
allowed types and the limits are described on the field). A `databaseSchema` that cannot be used is refused
before anything is created, with `400` and `INVALID_DATABASE_SCHEMA`. If the composition is created but its
tables cannot be made just now, the answer is still `201` and `meta.warnings` says so.

Leave `databaseSchema` out and the composition gets the tables its widget declares in its own source
(`src/lib/meta/database.json`), if it declares any: they are made the same way, in a schema of the
composition's own. A widget that keeps nothing leaves the composition without tables. If the widget's file
cannot be used, or its tables cannot be made just now, the answer is still `201` and `meta.warnings` says so.
The widget is read as it was when it was last published (as it is now, when it never was).

The `Idempotency-Key` header is required (`400` without it). Send the same key when you retry, so one
composition is created, not two.




## OpenAPI

````yaml /openapi/widgetic-api-public.json post /compositions
openapi: 3.0.0
info:
  title: Widgetic API Gateway
  version: 1.0.0
  description: API documentation for the Widgetic API Gateway
  license:
    name: Widgetic Integration License
    url: https://docs.widgetic.com/resources/license
servers:
  - url: https://api.widgetic.com/v1
    description: Production server
    variables:
      env:
        default: production
        description: Environment
      apiUrl:
        default: https://api.widgetic.com
        description: API URL
      apiVersion:
        default: /v1
        description: API Version path prefix
security: []
tags: []
paths:
  /compositions:
    post:
      tags:
        - Compositions
      summary: Create Composition
      description: >
        Creates a new composition.


        To make the composition for one of your managed users, send
        `managedUserId` or `managedUserExternalId`.

        The composition stays in your account and is yours to edit; the tag lets
        you list one user's compositions

        with `getAllCompositions`. The managed user must be yours and active:
        one that is not yours, or does not

        exist, is answered with 404 and `MANAGED_USER_NOT_FOUND`, and one that
        is switched off with 409 and

        `MANAGED_USER_INACTIVE`. The tag is set here and does not change
        afterwards. A copy made with

        `duplicateComposition` keeps the tag of the original.


        An editor session with the `creator` surface may call this to start a
        composition of a widget it built:

        it sends only `name`, `widgetId` (a widget made for the session's
        managed user) and `content`, and the

        composition is always made for that managed user.


        Send `databaseSchema` to give the composition tables of its own for what
        visitors send it (the shape, the

        allowed types and the limits are described on the field). A
        `databaseSchema` that cannot be used is refused

        before anything is created, with `400` and `INVALID_DATABASE_SCHEMA`. If
        the composition is created but its

        tables cannot be made just now, the answer is still `201` and
        `meta.warnings` says so.


        Leave `databaseSchema` out and the composition gets the tables its
        widget declares in its own source

        (`src/lib/meta/database.json`), if it declares any: they are made the
        same way, in a schema of the

        composition's own. A widget that keeps nothing leaves the composition
        without tables. If the widget's file

        cannot be used, or its tables cannot be made just now, the answer is
        still `201` and `meta.warnings` says so.

        The widget is read as it was when it was last published (as it is now,
        when it never was).


        The `Idempotency-Key` header is required (`400` without it). Send the
        same key when you retry, so one

        composition is created, not two.
      operationId: createComposition
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
        - $ref: '#/components/parameters/ManagedUserScopeId'
        - $ref: '#/components/parameters/ManagedUserScopeExternalId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewComposition'
      responses:
        '201':
          description: Composition created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompositionResponse'
        '400':
          description: >-
            Invalid input (including a `managedUserId` that is not a UUID, both
            `managedUserId` and `managedUserExternalId`, or a `databaseSchema`
            that cannot be used, `INVALID_DATABASE_SCHEMA`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: >-
            The managed user named by `managedUserId` or `managedUserExternalId`
            is not one of yours (`MANAGED_USER_NOT_FOUND`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            The managed user is switched off, so nothing new can be made for it
            (`MANAGED_USER_INACTIVE`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - apiKeyAuth: []
      x-code-samples:
        - lang: TypeScript
          label: TypeScript SDK
          source: >
            import { randomUUID } from 'node:crypto';

            import { Configuration, CompositionsApi, ResponseError } from
            '@widgetic/api-sdk-ts';


            const config = new Configuration({
              apiKey: 'YOUR_API_KEY'
            });


            const compositionsApi = new CompositionsApi(config);


            try {
              const composition = await compositionsApi.createComposition({
                // The call needs an idempotency key: reuse the same key if you retry this call.
                idempotencyKey: randomUUID(),
                newComposition: {
                  name: 'My New Composition',
                  widgetId: '123e4567-e89b-12d3-a456-426614174002',
                  content: {}
                }
              });
              
              console.log('Created composition:', composition);
            } catch (error) {
              if (error instanceof ResponseError && error.response.status === 400) {
                // The body of the answer says what was wrong
                console.error('Invalid input:', await error.response.json());
              } else {
                console.error('Failed to create composition:', error);
              }
            }
components:
  parameters:
    IdempotencyKeyRequired:
      in: header
      name: Idempotency-Key
      required: true
      description: >-
        Required: a call without it is refused with `400`. A key of your own
        that names this one request; a UUID is ideal. It is 8 to 50 characters
        of letters, digits, `-` and `_`. Send the same key again when you retry
        (after a timeout, a dropped connection or a `5xx`) and the call is
        carried out once: the retry gets the first answer back, marked
        `X-Idempotency-Cached: true`. The same key with a different body is
        refused with `422`, and a retry that arrives while the first call is
        still running with `409` (see `Retry-After`).
      schema:
        type: string
        minLength: 8
        maxLength: 50
        pattern: ^[a-zA-Z0-9_-]{8,50}$
        example: 9b2d6f0a-3c41-4e5a-8d77-1f2e3a4b5c6d
    ManagedUserScopeId:
      in: header
      name: X-Managed-User-Id
      required: false
      description: >-
        The id Widgetic gave the managed user (`POST /managed-users`). Makes
        this call for one of your managed users, so that a mistake in your own
        code cannot reach another customer's work. Send this header, or the
        other one, never both. Left out, the call works on your whole account,
        as it always has. With it, the call reaches only that user's
        compositions and workflows: a list shows only theirs, a composition or a
        workflow the call makes is filed under them, and one of another user, or
        one that does not exist, answers `404` (the same answer, so the call
        cannot be used to find out what another customer has). A managed user
        that is not yours answers `404` with `MANAGED_USER_NOT_FOUND`. An empty,
        repeated or malformed header is `400` with `MANAGED_USER_SCOPE_INVALID`;
        an empty header is never read as no header. A call that is about the
        whole account, such as the list of your managed users, is refused with
        `403` and `MANAGED_USER_SCOPE_ROUTE_NOT_AVAILABLE`: send it without the
        header. Only your API key can send it. The header is a limit the call
        asks for: whoever holds the key can leave it out, so for a credential
        that a customer's browser holds, use an editor session. An
        `Idempotency-Key` is kept apart for each managed user.
      schema:
        type: string
        format: uuid
        example: 3f2b8c1e-7a4d-4e0b-9c55-2d1a6b7e8f90
    ManagedUserScopeExternalId:
      in: header
      name: X-Managed-User-External-Id
      required: false
      description: >-
        Your own id for the managed user (its `externalId`), when you do not
        keep Widgetic's. Makes this call for one of your managed users, so that
        a mistake in your own code cannot reach another customer's work. Send
        this header, or the other one, never both. Left out, the call works on
        your whole account, as it always has. With it, the call reaches only
        that user's compositions and workflows: a list shows only theirs, a
        composition or a workflow the call makes is filed under them, and one of
        another user, or one that does not exist, answers `404` (the same
        answer, so the call cannot be used to find out what another customer
        has). A managed user that is not yours answers `404` with
        `MANAGED_USER_NOT_FOUND`. An empty, repeated or malformed header is
        `400` with `MANAGED_USER_SCOPE_INVALID`; an empty header is never read
        as no header. A call that is about the whole account, such as the list
        of your managed users, is refused with `403` and
        `MANAGED_USER_SCOPE_ROUTE_NOT_AVAILABLE`: send it without the header.
        Only your API key can send it. The header is a limit the call asks for:
        whoever holds the key can leave it out, so for a credential that a
        customer's browser holds, use an editor session. An `Idempotency-Key` is
        kept apart for each managed user.
      schema:
        type: string
        minLength: 1
        maxLength: 255
        example: customer-1042
  schemas:
    NewComposition:
      type: object
      properties:
        name:
          type: string
          description: Name of the composition
          example: My New Composition
        widgetId:
          type: string
          format: uuid
          description: ID of the widget this composition is based on
          example: 123e4567-e89b-12d3-a456-426614174002
        creatorId:
          type: string
          format: uuid
          description: ID of the user creating the composition
          example: 123e4567-e89b-12d3-a456-426614174001
        content:
          type: object
          description: Content data for the composition
          example:
            key: value
        status:
          type: string
          description: Initial status of the composition
          default: draft
          example: draft
        embedSettings:
          type: object
          description: Settings for embedding the composition
          example:
            width: 100%
            height: 500px
        design:
          type: object
          description: Design settings for the composition
          example:
            theme: light
            colors:
              primary: '#0066ff'
        databaseSchema:
          type: object
          description: >
            The tables this composition keeps what visitors send it in (votes,
            ratings, answers). They are made when

            the composition is created, in a schema of their own that no other
            composition can reach: in Widgetic's

            database, or in your own Neon database when you have set one with
            `setPartnerDataDatabase`. They are

            deleted with the composition.


            The shape is `{ "tables": [ ... ] }`. A table has a `name`, a list
            of `columns` and, optionally, a list of

            `indexes`, and optionally `countable`. A column has a `name` and a
            `type`, and optionally `primaryKey`,

            `notNull`, `unique`, `default` and `readable`. An index has a `name`
            and the `columns` it covers (1 to 8),

            and optionally `unique`. Any other key is refused, except `source`
            (see below).


            `countable` and `readable` are the read policy of the composition's
            own page: nothing the visitors sent

            can be read back by the page until you say so. `countable: true` on
            a table lets the widget show how many

            rows it has; `readable: true` on a column lets it show or total that
            column's values (a tally of votes,

            an average rating). Mark only what is safe to show to anyone who
            opens the page. A column whose name

            holds `email`, `phone`, `address`, `token`, `password`, `secret`,
            `ip` or the like, and a `json`, `jsonb`

            or `uuid` column, cannot be `readable`.


            - Names (tables, columns, indexes) use lower-case letters, digits
            and underscores, start with a letter and
              are at most 63 characters long. A name that starts with `_` or `pg_` is reserved.
            - `type` is one of `text`, `smallint`, `integer` (or `int`),
            `bigint`, `numeric`, `real`,
              `double precision`, `boolean` (or `bool`), `date`, `timestamp`, `timestamptz` (or
              `timestamp with time zone`), `uuid`, `json`, `jsonb`, `serial`, `bigserial`.
            - `default` is a number, `true` or `false`, a text of at most 500
            characters (no backslash), a JSON value
              for a `json` or `jsonb` column, or one of `now()`, `current_timestamp` and `current_date` (date and
              timestamp columns) and `gen_random_uuid()` (a `uuid` column). A `serial` column numbers itself and has
              no default.
            - A composition declares at most 20 tables, with at most 64 columns
            and 10 indexes each.


            A declaration that cannot be used is refused with `400` and
            `INVALID_DATABASE_SCHEMA`;

            `error.details.problems` lists what to change. Tables that already
            exist are never changed or dropped:

            sending more tables later, when you update the composition, adds the
            ones that are missing. When the

            composition is saved but its tables cannot be made just now, the
            answer says so in `meta.warnings`: send

            the same `databaseSchema` again to retry.


            Leave it out and the composition gets the tables its widget declares
            in its own source

            (`src/lib/meta/database.json`), if any. Send `{ "tables": [] }` to
            give the composition no tables and

            keep it from getting its widget's.


            A composition whose tables were made from its widget's own file
            reads back with `"source": "widget"` next

            to `tables`. When the widget is published again, a table, a field or
            an index its file gained is added to

            them; nothing that exists is changed or dropped, and what a page may
            read does not change. Send `source`

            back as it was and it stays. Leave it out and the tables are yours:
            no widget's file adds anything to

            them. It is the only value `source` can have.
          example:
            tables:
              - name: votes
                columns:
                  - name: id
                    type: serial
                    primaryKey: true
                  - name: choice
                    type: text
                    notNull: true
                  - name: created_at
                    type: timestamptz
                    default: now()
                indexes:
                  - name: votes_choice_idx
                    columns:
                      - choice
        projectId:
          type: string
          format: uuid
          description: ID of the project this composition belongs to
          example: 123e4567-e89b-12d3-a456-426614174004
        managedUserId:
          type: string
          format: uuid
          description: >-
            Make the composition for one of your managed users, by Widgetic's id
            for the user. Send this or `managedUserExternalId`, not both. The
            managed user must be yours and active. The composition stays in your
            account; the tag lets you list one user's compositions and keeps it
            out of the others' lists. It is set here and does not change
            afterwards.
          example: b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e
        managedUserExternalId:
          type: string
          maxLength: 255
          description: >-
            The same, by your own identifier for the user, the `externalId` you
            gave when you created the managed user (answers show it as
            `externalId`). Send this or `managedUserId`, not both.
          example: user-8841
      required:
        - name
        - widgetId
      example:
        name: My New Composition
        widgetId: 123e4567-e89b-12d3-a456-426614174002
        creatorId: 123e4567-e89b-12d3-a456-426614174001
        content:
          key: value
        status: draft
        embedSettings:
          width: 100%
          height: 500px
        design:
          theme: light
          colors:
            primary: '#0066ff'
        databaseSchema:
          tables:
            - name: votes
              columns:
                - name: id
                  type: serial
                  primaryKey: true
                - name: choice
                  type: text
                  notNull: true
                - name: created_at
                  type: timestamptz
                  default: now()
              indexes:
                - name: votes_choice_idx
                  columns:
                    - choice
        projectId: 123e4567-e89b-12d3-a456-426614174004
        managedUserId: b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e
        managedUserExternalId: user-8841
    CompositionResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          description: Always `true` on a success.
          example: true
        data:
          $ref: '#/components/schemas/Composition'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    ErrorResponse:
      type: object
      required:
        - success
        - message
        - error
      additionalProperties: true
      properties:
        success:
          type: boolean
          description: Always `false` on a failure.
          example: false
        message:
          type: string
          description: The same text as `error.message`, for a caller that reads one line.
        error:
          $ref: '#/components/schemas/ErrorDetail'
    Composition:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the composition
          example: 123e4567-e89b-12d3-a456-426614174000
        creatorId:
          type: string
          format: uuid
          description: ID of the user who created the composition
          example: 123e4567-e89b-12d3-a456-426614174001
        widgetId:
          type: string
          format: uuid
          description: ID of the widget this composition is based on
          example: 123e4567-e89b-12d3-a456-426614174002
        name:
          type: string
          description: Name of the composition
          example: My Composition
        content:
          type: object
          description: Content data for the composition
          example:
            key: value
        contentVersion:
          type: integer
          description: >-
            Monotonic content version when the content_version migration is
            applied
          example: 3
        status:
          type: string
          description: Current status of the composition
          enum:
            - draft
            - published
            - archived
          example: draft
        createdAt:
          type: string
          format: date-time
          description: When the composition was created
          example: '2023-01-01T00:00:00Z'
        updatedAt:
          type: string
          format: date-time
          description: When the composition was last updated
          example: '2023-01-02T00:00:00Z'
        embedSettings:
          type: object
          description: Settings for embedding the composition
          example:
            width: 100%
            height: 500px
        design:
          type: object
          description: Design settings for the composition
          example:
            theme: light
            colors:
              primary: '#0066ff'
        databaseId:
          type: string
          format: uuid
          description: >-
            Legacy. The id of a Neon database that an earlier version made for
            this composition. A composition now keeps its tables in a schema of
            its own (named `comp_` followed by its id) in a database shared with
            other compositions, so new compositions have none.
          example: 123e4567-e89b-12d3-a456-426614174003
        projectId:
          type: string
          format: uuid
          description: ID of the project this composition belongs to
          example: 123e4567-e89b-12d3-a456-426614174004
        managedUserId:
          type: string
          format: uuid
          nullable: true
          description: >-
            The managed user the composition was made for, or null when it is
            your own. List one managed user's compositions with the
            `managedUserId` or `managedUserExternalId` query parameter of
            `getAllCompositions`.
          example: b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e
        direct:
          type: boolean
          nullable: true
          description: >
            Whether the widget the composition is made from is drawn straight
            into the page, without a frame

            (`true`), or inside a frame (`false`). `null` when the widget has no
            published version yet. It is a

            fact about the widget's published version, so every composition of
            one widget answers the same, and it

            changes when you publish another version. It cannot be set. Only
            `getCompositionById` returns it:

            the other calls that return a composition leave it out, and so does
            that one on the rare occasion the

            answer could not be worked out just then (ask again).
          example: true
        lastEditedBy:
          type: object
          nullable: true
          description: >
            Who made the latest change to a composition a team shares (one filed
            under a project of an organization,

            or one made in a Shopify store's workspace), so that a teammate who
            opens it can be told "Anna changed

            this at 14:32" and a save that someone else got ahead of can say
            who. It is `null` when nobody can be

            named: a change made by a Widgetic service, through an editor
            session, by a partner's call made for one

            managed user, or one made before this was recorded.


            Only `getCompositionById` returns it, and only for a composition a
            team shares, and only to a person (a

            signed-in member or a key of one): a composition in a personal
            project leaves it out, and so does every

            answer given to an editor session or to a call made for one managed
            user. It is also left out on the rare

            occasion the team could not be worked out just then (ask again). The
            other calls that return a

            composition leave it out.


            It is not the same as `creatorId`, who made the composition, and the
            account behind a change is never

            part of the composition itself: this is the only place it is told.
          properties:
            id:
              type: string
              format: uuid
              description: The account that made the latest change
              example: 123e4567-e89b-12d3-a456-426614174001
            name:
              type: string
              nullable: true
              maxLength: 120
              description: >-
                The name on that account, or `null` when it has none or it could
                not be read just then. Never an email address.
              example: Anna
        lastEditedAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            When that latest change was made. Present whenever `lastEditedBy`
            is.
          example: '2026-10-28T14:32:00Z'
      required:
        - id
        - creatorId
        - widgetId
        - name
        - content
        - status
        - createdAt
        - updatedAt
      example:
        id: 123e4567-e89b-12d3-a456-426614174000
        creatorId: 123e4567-e89b-12d3-a456-426614174001
        widgetId: 123e4567-e89b-12d3-a456-426614174002
        name: My Composition
        content:
          key: value
        contentVersion: 3
        status: draft
        createdAt: '2023-01-01T00:00:00Z'
        updatedAt: '2023-01-02T00:00:00Z'
        embedSettings:
          width: 100%
          height: 500px
        design:
          theme: light
          colors:
            primary: '#0066ff'
        databaseId: 123e4567-e89b-12d3-a456-426614174003
        projectId: 123e4567-e89b-12d3-a456-426614174004
        managedUserId: b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e
        direct: true
        lastEditedBy: {}
        lastEditedAt: '2026-10-28T14:32:00Z'
    ResponseMeta:
      type: object
      description: >-
        Extra information about the answer: where a page sits in a list, the
        cursor of the next page, or warnings about a call that succeeded with a
        caveat.
      additionalProperties: true
      properties:
        page:
          type: integer
          description: The page that was read, counting from 1.
          example: 1
        size:
          type: integer
          description: How many items one page holds.
          example: 20
        total:
          type: integer
          description: How many items the whole list has.
          example: 135
        limit:
          type: integer
          description: The same as `size`.
          example: 20
        offset:
          type: integer
          description: How many items come before this page.
          example: 0
        hasNextPage:
          type: boolean
          example: true
        hasPreviousPage:
          type: boolean
          example: false
        nextCursor:
          type: string
          nullable: true
          description: >-
            Pass this as `cursor` to read the next page of a list that is read
            by cursor. `null` on the last page.
        warnings:
          type: array
          items:
            type: string
          description: >-
            Things the call did not do, though it succeeded (for example a table
            that could not be created just now).
    ErrorDetail:
      type: object
      required:
        - code
        - message
        - timestamp
      properties:
        code:
          type: string
          description: Machine-readable error code.
          example: RESOURCE_NOT_FOUND
        message:
          type: string
          description: Human-readable explanation.
          example: Composition not found
        param:
          type: string
          description: The field that caused the error, when one did.
          example: name
        docUrl:
          type: string
          description: Where this error is explained.
        suggestedAction:
          type: string
          description: What to do about it.
        retryAfter:
          type: integer
          description: >-
            Seconds to wait before trying again (rate limits). The same number
            is in the `Retry-After` header.
          example: 60
        details:
          type: object
          additionalProperties: true
          description: >-
            More about the error: the fields that failed, a limit that was
            reached.
        correlationId:
          type: string
          description: Quote this when you ask support about the call.
          example: req_123e4567-e89b-12d3-a456-426614174000
        timestamp:
          type: string
          format: date-time
          example: '2026-10-15T10:00:00.000Z'
  securitySchemes:
    apiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.