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

# List a site's embeddable widgets

> Every embeddable widget on ONE site — forms, booking calendars, shops, AI chat agents and WhatsApp launchers — shaped for a picker. This is the only operation that authenticates with a **site key** rather than an API key: pass the site's `sor_site_...` value in the `X-Sorraia-Site-Key` header. Find it in the Sorraia dashboard under Site → Embed.

A site key grants this one read on its one site and nothing else. It cannot read submissions, bookings, orders or any other resource, and it is never accepted as an API key.

`embedId` is the identifier that widget's embed URL actually takes: the numeric widget id for forms, calendars, shops and WhatsApp, and the string `publicId` for AI chat. Archived widgets are omitted entirely; paused widgets are listed with `paused: true`. There is no pagination — the full catalogue is returned, ordered by type then name.




## OpenAPI

````yaml /api/openapi.yaml get /site-catalog
openapi: 3.1.0
info:
  title: Sorraia API
  version: 1.0.0
  description: >
    The Sorraia public API (v1). Read and manage your forms, submissions,
    calendars, bookings, shop widgets, and orders programmatically.

    Authenticate with an API key created in the Sorraia dashboard (Settings →
    Integrations, under Programmatic API access). Pass it as a Bearer token:
    `Authorization: Bearer sor_live_...`. Keys are scoped — a key only reaches
    the endpoints its scopes allow (e.g. `read:forms`, `write:submissions`).

    API access is included with every paid Sorraia plan. A key whose account is
    on the Free plan receives `402` with `code: plan_feature_gated` (`feature:
    apiAccess`) on every endpoint that takes an API key, except `DELETE
    /webhooks/{id}` — unsubscribing always works. Upgrading lifts it on the next
    request; keys are never revoked. The site-key endpoints (`/site-catalog`,
    `/site-verification` and `/site-verification/attempt`) are not affected.

    All responses are JSON. List endpoints return a `{ data, pagination }`
    envelope; single-resource endpoints return the object directly. Requests are
    rate-limited per API key; see the `X-RateLimit-*` response headers.
  contact:
    name: Sorraia Support
    url: https://sorraia.app
  license:
    name: Proprietary
    url: https://sorraia.app/terms
servers:
  - url: https://api.sorraia.app/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: forms
    description: Create, read, update, and archive forms.
  - name: submissions
    description: Read and programmatically ingest form submissions.
  - name: calendars
    description: Read calendar widgets.
  - name: bookings
    description: Read calendar bookings.
  - name: ecommerce
    description: Read shop widgets.
  - name: orders
    description: Read shop orders.
  - name: site-catalog
    description: >
      List one site's embeddable widgets. Authenticated by a per-site key rather
      than an API key — built for site-builder plugins (WordPress and friends)
      that need a widget picker, not a data feed.
  - name: site-verification
    description: >
      Prove ownership of a site's domain from inside the merchant's own
      WordPress admin. Authenticated by a per-site key rather than an API key.
      Reading the token is a read; the attempt endpoint asks Sorraia to fetch
      that token back from the hostname being claimed — the key never asserts
      verification itself.
  - name: webhooks
    description: Manage event subscriptions (REST Hooks) for integrations.
paths:
  /site-catalog:
    get:
      tags:
        - site-catalog
      summary: List a site's embeddable widgets
      description: >
        Every embeddable widget on ONE site — forms, booking calendars, shops,
        AI chat agents and WhatsApp launchers — shaped for a picker. This is the
        only operation that authenticates with a **site key** rather than an API
        key: pass the site's `sor_site_...` value in the `X-Sorraia-Site-Key`
        header. Find it in the Sorraia dashboard under Site → Embed.


        A site key grants this one read on its one site and nothing else. It
        cannot read submissions, bookings, orders or any other resource, and it
        is never accepted as an API key.


        `embedId` is the identifier that widget's embed URL actually takes: the
        numeric widget id for forms, calendars, shops and WhatsApp, and the
        string `publicId` for AI chat. Archived widgets are omitted entirely;
        paused widgets are listed with `paused: true`. There is no pagination —
        the full catalogue is returned, ordered by type then name.
      operationId: getSiteCatalog
      responses:
        '200':
          description: The site and its embeddable widgets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteCatalog'
        '401':
          description: No `X-Sorraia-Site-Key` header was sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >
            The site key is unknown, has been regenerated, or the site is
            archived. Returned instead of 401 so the response cannot be used to
            probe which keys exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - siteKeyAuth: []
components:
  schemas:
    SiteCatalog:
      type: object
      required:
        - site
        - widgets
      properties:
        site:
          type: object
          required:
            - name
            - domain
          properties:
            name:
              type: string
              description: >
                The site's display label. Today this equals `domain` — a site
                has no separate name — but it is its own field so a client can
                bind to it and keep working if that changes.
            domain:
              type: string
              description: The registered hostname, e.g. `example.com`.
        widgets:
          type: array
          description: >
            Every non-archived embeddable widget on the site, ordered by type
            (form, calendar, shop, chat, whatsapp) then name. Not paginated.
          items:
            $ref: '#/components/schemas/SiteCatalogWidget'
    Error:
      type: object
      description: >
        Standard error envelope. `code` is a stable machine-readable string
        (e.g. invalid_api_key, insufficient_scope, not_found, rate_limited,
        domain_required, forbidden_field, plan_feature_gated,
        plan_limit_reached). Some errors carry extra context fields (candidates,
        required, fields, invalid, retryAfter, feature, resource, cap, used,
        domainId).
      required:
        - error
      additionalProperties: true
      properties:
        error:
          type: string
          description: Human-readable message.
        code:
          type: string
          description: Stable machine-readable error code.
    SiteCatalogWidget:
      type: object
      description: >
        One embeddable widget, shaped for a picker. Deliberately narrow: a
        label, a type, the identifier its embed takes, and whether it is
        currently serving. No content, no pricing, no configuration.
      required:
        - type
        - name
        - embedId
        - paused
      properties:
        type:
          type: string
          enum:
            - form
            - calendar
            - shop
            - chat
            - whatsapp
          description: Which embed loader renders this widget.
        name:
          type: string
          description: The merchant-facing name, as shown in the dashboard.
        embedId:
          oneOf:
            - type: integer
            - type: string
          description: >
            The identifier this widget's embed URL takes — a numeric widget id
            for form, calendar, shop and whatsapp; the string `publicId` for
            chat, whose loader is `/api/embed/chat/{publicId}.js`. A chat
            widget's numeric primary key is never returned, because no embed can
            use it.
        paused:
          type: boolean
          description: >
            True when the merchant has paused the widget. A paused widget still
            exists and still appears here, but its embed serves an inert stub —
            surface it in the picker so a merchant can find what they paused.
            Archived widgets are omitted from the catalogue entirely.
  responses:
    RateLimited:
      description: The per-API-key request budget for the current window is exhausted.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: An unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  headers:
    RateLimitLimit:
      description: The per-API-key request budget for the current 1-hour window.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests remaining in the current window after this request.
      schema:
        type: integer
    RateLimitReset:
      description: Unix timestamp (seconds) at which the rate-limit window resets.
      schema:
        type: integer
    RetryAfter:
      description: Seconds to wait before retrying. Present on 429 responses.
      schema:
        type: integer
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sor_live_*
      description: >
        A Sorraia API key created in the dashboard (Settings → Integrations,
        under Programmatic API access). Pass it as `Authorization: Bearer
        sor_live_...`. API access is included with every paid plan; a key whose
        account is on the Free plan receives `402` `plan_feature_gated`.
    siteKeyAuth:
      type: apiKey
      in: header
      name: X-Sorraia-Site-Key
      description: >
        A per-site key (`sor_site_...`), shown on the site's Embed card in the
        dashboard (Site → Embed) and regenerable there. It authorises three
        operations for the single site it belongs to — `GET /site-catalog`, `GET
        /site-verification` and `POST /site-verification/attempt` — and nothing
        else. It is NOT an API key: it carries no scopes, reads no customer
        data, and `Authorization: Bearer` will not accept it.


        It is not purely read-only: the attempt endpoint can mark the site's
        domain verified. It does so only by asking Sorraia to fetch the site's
        verification token back from the registered hostname — the key proves
        account access, never hostname control, so it cannot claim a domain it
        cannot serve the token from.

````