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

# Append a visitor entry through the gateway

> Public append-only write into a published composition database. No API key. The browser must send a permitted `Origin` header: the artifact iframe origin, or any website when the owner has not restricted the allowed websites (when the owner has, one of the websites the owner listed, Widgetic's own sites, the visitor's own machine or the owner's own platform). When the request comes from the shared artifact origin, it must also carry the per-composition write token (`X-Widgetic-Write-Token` header or `writeToken` body field, handed to the widget in `widgetic:host` as `entries.writeToken` and returned as `entryWriteToken` on `getPublicCompositionEmbed`). Host `context` is stored for partner attribution; it is not authenticated unless the partner signs it (see **Signed context**). Request bodies are limited to 16 KB before parsing.
**Address.** `https://api.widgetic.com/embed/compositions/{compositionId}/entries/{table}`: the root of the API host, NOT under `/v1`. A client generated from this description for `https://api.widgetic.com/v1` must be given the base address `https://api.widgetic.com` for the calls of the `Public Embed` tag.
**Signed context.** A partner that wants to trust `context` (to bill a customer by it, say) makes a signing secret (`rotatePartnerEntryContextSecret`), and its server signs the context it hands to the embed. The signature travels inside `context` as two reserved keys, `widgetic_exp` (UTC seconds, at most seven days ahead) and `widgetic_sig`. The gateway checks them after the `Origin` and before anything else is judged, stored or announced, and keeps and announces the context WITHOUT them. A context that is not signed as the partner requires is refused with 403 `CONTEXT_SIGNATURE_REQUIRED`, `CONTEXT_SIGNATURE_INVALID` or `CONTEXT_SIGNATURE_EXPIRED`; when the check cannot be made just now the answer is a retryable 503 `CONTEXT_SIGNATURE_UNAVAILABLE`, never an unchecked entry. Until a partner makes a secret nothing changes: the two keys are dropped and the rest is taken as it came. A signature is good until its `widgetic_exp` and may be used again until then, so a partner that must count each submission once keeps a unique value of its own (a `submissionId`, say) in the context. The recipe and a test vector are in the description of `rotatePartnerEntryContextSecret`.



## OpenAPI

````yaml /openapi/widgetic-api-public.json post /embed/compositions/{compositionId}/entries/{table}
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:
  /embed/compositions/{compositionId}/entries/{table}:
    servers:
      - url: https://api.widgetic.com
        description: >-
          The public embed routes are served at the root of the API host, not
          under /v1.
    post:
      tags:
        - Public Embed
      summary: Append a visitor entry through the gateway
      description: >-
        Public append-only write into a published composition database. No API
        key. The browser must send a permitted `Origin` header: the artifact
        iframe origin, or any website when the owner has not restricted the
        allowed websites (when the owner has, one of the websites the owner
        listed, Widgetic's own sites, the visitor's own machine or the owner's
        own platform). When the request comes from the shared artifact origin,
        it must also carry the per-composition write token
        (`X-Widgetic-Write-Token` header or `writeToken` body field, handed to
        the widget in `widgetic:host` as `entries.writeToken` and returned as
        `entryWriteToken` on `getPublicCompositionEmbed`). Host `context` is
        stored for partner attribution; it is not authenticated unless the
        partner signs it (see **Signed context**). Request bodies are limited to
        16 KB before parsing.

        **Address.**
        `https://api.widgetic.com/embed/compositions/{compositionId}/entries/{table}`:
        the root of the API host, NOT under `/v1`. A client generated from this
        description for `https://api.widgetic.com/v1` must be given the base
        address `https://api.widgetic.com` for the calls of the `Public Embed`
        tag.

        **Signed context.** A partner that wants to trust `context` (to bill a
        customer by it, say) makes a signing secret
        (`rotatePartnerEntryContextSecret`), and its server signs the context it
        hands to the embed. The signature travels inside `context` as two
        reserved keys, `widgetic_exp` (UTC seconds, at most seven days ahead)
        and `widgetic_sig`. The gateway checks them after the `Origin` and
        before anything else is judged, stored or announced, and keeps and
        announces the context WITHOUT them. A context that is not signed as the
        partner requires is refused with 403 `CONTEXT_SIGNATURE_REQUIRED`,
        `CONTEXT_SIGNATURE_INVALID` or `CONTEXT_SIGNATURE_EXPIRED`; when the
        check cannot be made just now the answer is a retryable 503
        `CONTEXT_SIGNATURE_UNAVAILABLE`, never an unchecked entry. Until a
        partner makes a secret nothing changes: the two keys are dropped and the
        rest is taken as it came. A signature is good until its `widgetic_exp`
        and may be used again until then, so a partner that must count each
        submission once keeps a unique value of its own (a `submissionId`, say)
        in the context. The recipe and a test vector are in the description of
        `rotatePartnerEntryContextSecret`.
      operationId: createPublicCompositionEntry
      parameters:
        - in: path
          name: compositionId
          required: true
          schema:
            type: string
            format: uuid
        - in: path
          name: table
          required: true
          schema:
            type: string
        - in: header
          name: X-Widgetic-Write-Token
          required: false
          description: >-
            Signed per-composition write token handed to the widget in
            `widgetic:host` as `entries.writeToken` (and returned by `GET
            /embed/compositions/{compositionId}` as `entryWriteToken`). Required
            when the request comes from the shared Widgetic artifact origin;
            verified whenever present.
          schema:
            type: string
        - in: header
          name: X-Widgetic-Visitor-Id
          required: false
          description: >-
            A random id the widget's browser makes once and keeps (16 to 64
            characters of `A-Za-z0-9_-`), sent so a table's per-visitor `limit`
            (declared in the widget database file) can count this browser. Not
            an identity and not a secret. Without it, or with a malformed value,
            the limit counts only by network address.
          schema:
            type: string
            pattern: ^[A-Za-z0-9_-]{16,64}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCompositionEntryRequest'
      responses:
        '201':
          description: Entry stored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCompositionEntryCreated'
        '400':
          description: >-
            Invalid body or values (`ENTRY_BODY_INVALID`, `ENTRY_VALUE_INVALID`,
            `ENTRY_CONTEXT_INVALID`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '403':
          description: >-
            Origin not allowed (`ORIGIN_NOT_ALLOWED`), per-composition write
            token missing, invalid or expired (`WRITE_TOKEN_REQUIRED`,
            `WRITE_TOKEN_INVALID`, `WRITE_TOKEN_EXPIRED`), or the context is not
            signed as the partner requires (`CONTEXT_SIGNATURE_REQUIRED`,
            `CONTEXT_SIGNATURE_INVALID`, `CONTEXT_SIGNATURE_EXPIRED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '404':
          description: Composition unavailable or table not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '413':
          description: Body too large (`PAYLOAD_TOO_LARGE`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '415':
          description: Not JSON (`UNSUPPORTED_MEDIA_TYPE`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '422':
          description: Row rejected (`ENTRY_ROW_REJECTED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '429':
          description: >-
            Rate limited (`RATE_LIMITED`), always with `Retry-After`. Three
            limits answer this way: 20 entries a minute per visitor address and
            composition, 300 a minute per composition, and a day's allowance per
            composition (default 50,000 stored entries in 24 hours;
            `details.scope` is `composition_daily` for that one). Only an entry
            that was stored counts against the day's allowance. A fourth answers
            a table that declares a per-visitor `limit`: `error.code` is
            `ENTRY_LIMIT_REACHED`, `details.scope` is `visitor_limit` (this
            browser) or `address_limit` (this network address), and
            `details.perVisitor` and `details.per` repeat what the table
            declares.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
        '503':
          description: >-
            Database unavailable or statement timeout, or the gateway could not
            tell just now whether the context must be signed
            (`CONTEXT_SIGNATURE_UNAVAILABLE`). Try again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEmbedFailure'
components:
  schemas:
    PublicCompositionEntryRequest:
      type: object
      required:
        - values
      properties:
        values:
          type: object
          additionalProperties: true
          description: Column values to insert (never id or time columns).
        context:
          type: object
          nullable: true
          description: >-
            Host attribution from the embedding page. It is not authenticated
            unless the partner signs it (see the description of this operation):
            never use it for authorization on its own. At most 20 keys; string
            values up to 500 characters or finite numbers; 2048 bytes JSON
            total. A signed context carries two more keys, `widgetic_exp` and
            `widgetic_sig`: the gateway checks them and drops them, so they are
            neither stored nor announced.
        writeToken:
          type: string
          nullable: true
          description: >-
            Optional per-composition write token (`entries.writeToken` from
            `widgetic:host`, or `entryWriteToken` from `GET
            /embed/compositions/{compositionId}`) when not passed in the
            `X-Widgetic-Write-Token` header.
      example:
        values: {}
        context: {}
        writeToken: Example writeToken
    PublicCompositionEntryCreated:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - entryId
            - table
            - createdAt
          properties:
            entryId:
              type: string
            table:
              type: string
            createdAt:
              type: string
              format: date-time
      example:
        success: true
        data: {}
    PublicEmbedFailure:
      type: object
      required:
        - success
        - message
        - error
        - code
      description: >-
        What a public embed route answers when it refuses or fails. It is the
        standard error answer of the API (`success`, `message`, `error`), and
        the short `code` (with `details` and `correlationId` when the route has
        them) is also at the top level, because a page that embeds a composition
        branches on it there. `error.code` is the same word in the codes of the
        rest of the API where those have it: `NOT_FOUND` is
        `RESOURCE_NOT_FOUND`, `RATE_LIMITED` is `RATE_LIMIT_EXCEEDED`, and the
        others (`DOMAIN_NOT_ALLOWED`, `UNPUBLISHED`, `SUSPENDED`, the codes of
        the entry route) are the same in both places.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
          description: What happened, for a person.
        code:
          type: string
          description: >-
            The short code a page branches on: `DOMAIN_NOT_ALLOWED`,
            `UNPUBLISHED`, `SUSPENDED`, `NOT_FOUND`, `RATE_LIMITED`, and for the
            entry route the codes listed with its answers.
          example: NOT_FOUND
        details:
          type: object
          additionalProperties: true
          description: >-
            What tells the cases of one code apart, such as `reason` of
            `DOMAIN_NOT_ALLOWED`.
          properties:
            reason:
              type: string
              description: For `DOMAIN_NOT_ALLOWED`, why the website was refused.
              enum:
                - domain_not_allowed
                - opaque_origin
            hasDomainsConfigured:
              type: boolean
              description: >-
                For `DOMAIN_NOT_ALLOWED`, always true (an owner who listed
                nothing is never refused).
        correlationId:
          type: string
          description: The id of the request, for the routes that carry it.
        error:
          type: object
          required:
            - code
            - message
            - timestamp
          properties:
            code:
              type: string
              example: RESOURCE_NOT_FOUND
            message:
              type: string
            details:
              type: object
              additionalProperties: true
            correlationId:
              type: string
            retryAfter:
              type: integer
              description: Seconds to wait before asking again, for a rate-limit refusal.
            timestamp:
              type: string
              format: date-time
      example:
        success: true
        message: Example message
        code: NOT_FOUND
        details: {}
        correlationId: Example correlationId
        error: {}

````

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