> ## 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.

# Verify this site's domain via the WordPress plugin

> Ask Sorraia to check the `wp_plugin` proof now. The method is fixed — this endpoint cannot trigger a `dns_txt` or `meta_tag` check — and the request takes no body.

Sorraia fetches the site's own registered hostname over HTTPS, trying `/?rest_route=/sorraia/v1/verify` then `/wp-admin/admin-ajax.php?action=sorraia_verify`, each with a cache-busting parameter, and looks for the site's verification token. Redirects are followed only within the registered hostname and its www/apex twin. On an exact token match the site is marked verified and its method is set to `wp_plugin`.

The site key never asserts verification — it only asks for the check to run. The token still has to be found at the hostname being claimed.

Requires the Sorraia WordPress plugin to be installed and connected with this site's key. Not available on subdirectory installs or subdirectory multisite, where the plugin does not sit at the hostname root — use the DNS record there.




## OpenAPI

````yaml /api/openapi.yaml post /site-verification/attempt
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-verification/attempt:
    post:
      tags:
        - site-verification
      summary: Verify this site's domain via the WordPress plugin
      description: >
        Ask Sorraia to check the `wp_plugin` proof now. The method is fixed —
        this endpoint cannot trigger a `dns_txt` or `meta_tag` check — and the
        request takes no body.


        Sorraia fetches the site's own registered hostname over HTTPS, trying
        `/?rest_route=/sorraia/v1/verify` then
        `/wp-admin/admin-ajax.php?action=sorraia_verify`, each with a
        cache-busting parameter, and looks for the site's verification token.
        Redirects are followed only within the registered hostname and its
        www/apex twin. On an exact token match the site is marked verified and
        its method is set to `wp_plugin`.


        The site key never asserts verification — it only asks for the check to
        run. The token still has to be found at the hostname being claimed.


        Requires the Sorraia WordPress plugin to be installed and connected with
        this site's key. Not available on subdirectory installs or subdirectory
        multisite, where the plugin does not sit at the hostname root — use the
        DNS record there.
      operationId: attemptSiteVerification
      responses:
        '200':
          description: Verified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteVerificationResult'
        '400':
          description: >
            The check ran and did not pass. `code` is `wp_plugin_fetch_failed`
            (no candidate path answered — usually the plugin is not installed,
            or a security plugin is blocking both the REST API and `/wp-admin/`)
            or `verify_failed_no_plugin_token` (a path answered but the token
            was absent or did not match — usually the plugin is connected with a
            different site's key). `attempts` breaks the outcome down per path.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteVerificationFailure'
        '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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - siteKeyAuth: []
components:
  schemas:
    SiteVerificationResult:
      type: object
      required:
        - domain
        - verified
        - method
      properties:
        domain:
          type: string
        verified:
          type: boolean
          description: Always `true` — a failed check returns 400.
        method:
          type: string
          description: Always `wp_plugin` from this endpoint.
        verifiedAt:
          type: string
          format: date-time
        lastCheckedAt:
          type: string
          format: date-time
    SiteVerificationFailure:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
        code:
          type: string
          enum:
            - wp_plugin_fetch_failed
            - verify_failed_no_plugin_token
        method:
          type: string
          description: Always `wp_plugin`.
        attempts:
          type: array
          description: >
            One entry per candidate path tried, in order. Every entry describes
            a fetch of the site's OWN registered hostname — redirects off that
            host are refused, so this can never report on anywhere else.
          items:
            type: object
            properties:
              path:
                type: string
                description: The path probed.
              ok:
                type: boolean
              reason:
                type: string
                description: >
                  Why the path did not satisfy the check — a transport error,
                  `no_token_in_response`, or `token_mismatch`.
    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.
  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.

````