Frase Sites API

Manage websites

GitHub OpenAPI

Operations 5

GET /sites/{siteId}/cms-token Get a site's FraseCMS delivery token #
POST /sites/{siteId}/cms-token/rotate Rotate a site's FraseCMS delivery token #
GET /sites List sites #
POST /sites Create site #
GET /sites/{siteId}/cms-connections List CMS connections #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/frase-io:frase-io-sites-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

frase-io-sites-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Frase Sites API
  version: 1.0.0
  description: 'The Frase API provides programmatic access to your SEO content platform. Use it to manage content, research, audits, AI visibility tracking, and more. ## Authentication


    All API requests require authentication using an API key. Include your key in the `X-API-KEY` header:


    ```

    X-API-KEY: sk_live_your_api_key_here

    ```


    For legacy compatibility, the `token` header is also supported. ## Rate Limits


    Three independent limits apply, each with its own headers and error code. Headers are returned on `429` responses, not on successful ones. **Per-minute burst** - 60/min on trial and Starter, 120 Professional, 300 Scale, 1,000 Enterprise. Error code `rate_limit_exceeded`. - `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (epoch seconds)


    **Sustained hourly ceiling** - 2,000/hour trial, 3,000 Starter, 5,000 Professional, 15,000 Scale, 50,000 Enterprise. Error code `hourly_limit_exceeded`. - `X-RateLimit-Hourly-Limit`, `X-RateLimit-Hourly-Remaining`, `X-RateLimit-Hourly-Reset` (epoch seconds)


    **Monthly request budget** - error code `monthly_budget_exceeded`, headers `X-Monthly-Limit`, `X-Monthly-Used`, `X-Monthly-Remaining`. Every `429` carries `Retry-After` in seconds. On the hourly ceiling this can be up to 3,600, so clients should honour it rather than retrying on a short fixed backoff - a retry sooner than `Retry-After` cannot succeed. ## Errors


    The API uses standard HTTP status codes and returns errors in a consistent format:


    ```json

    {

    "error": {

    "type": "authentication_error",

    "code": "invalid_api_key",

    "message": "Invalid API key provided.",

    "doc_url": "https://docs.frase.io/api/errors#invalid_api_key"

    }

    }

    ```


    ## Changelog


    Breaking changes are listed here, newest first. "Breaking" means a field that was always present with a value can now be `null`, or an existing value now means something different — not that a field was removed or renamed. Additive fields are not listed. ### 2026-08-31 — Fact `sources[]` may omit `url`, and minted facts default to `approved`


    - `GET /briefs/{id}/facts`, `PATCH /briefs/{id}/facts/{factId}` → a `sources[]` entry may now carry only `title`, with no `url` or `domain`. Research names a publisher far more often than it captures a link (793 of 801 plan-evidence items in production), and those names were previously discarded rather than returned. Read `url` defensively. - `GET /briefs/{id}/facts` → `approval` on a newly minted fact is now `approved` rather than `pending`. A client polling for `pending` facts to review will see far fewer; rejection is unchanged and remains permanent. Facts the writer asserted in its own output (`provenance: generated`) are still minted `pending`. ### 2026-08-24 — Content Guard''s hasPublishedUrl is now recoverability, not the stored URL


    - `GET /content/{id}/monitoring` → `hasPublishedUrl`: was a judgment on the STORED URL (false for a relative path even when the page was live). It now answers "could Guard watch this article right now" — the stored URL when absolute, or an address the server recovers (CMS inventory; FraseCMS re-derivation from current settings when a live published post exists). `true` no longer implies the stored URL itself is absolute, and a FraseCMS article published before domain setup flips to `true` when setup finishes, with no re-publish. It remains exactly the condition enabling enforces. - `publishedUrlIssue` is now the RESIDUAL after recovery failed, not a raw-column classification — "not_absolute" can also mean the FraseCMS post is not live or FraseCMS is disabled for the site. - `POST /content/{id}/publish` → `url` (frase_cms): now derived from the site''s current CMS settings rather than the adapter''s host-of-the-moment value; the same URL is stored on the content row and sent in the content.published webhook.'
  contact:
    name: Frase Support
    url: https://frase.io
    email: support@frase.io
  license:
    name: Proprietary
    url: https://www.frase.io/company/terms
servers:
- url: https://next.frase.io/api/v1
  description: Production
- url: /api/v1
  description: This host
security:
- ApiKeyAuth: []
- BearerAuth: []
tags:
- name: Sites
  description: Manage websites
paths:
  /sites/{siteId}/cms-token:
    get:
      operationId: getSiteCmsDeliveryToken
      summary: Get a site's FraseCMS delivery token
      description: 'Returns the site''s CURRENT headless delivery token — the bearer credential the public FraseCMS read endpoints accept. Non-mutating: the token is rebuilt (HMAC) from the site''s stored secret and version, so repeated calls return the SAME token. It is therefore NOT a show-once secret: as long as the site''s secret exists you can fetch the live token again here. Only rotation (POST /sites/{siteId}/cms-token/rotate) changes it, and only revocation removes it. If the token has been revoked (the site''s secret was nulled), this returns 200 with `{ "revoked": true }` and nothing else — the endpoint NEVER re-mints a revoked token; re-issue explicitly by rotating. Gates: requires the `write:sites` scope even though the verb is GET, because the response reveals a live credential — a read-only key is refused with 403. Responses carry `Cache-Control: no-store`; never cache or log the token. An org-scoped bucket allows 60 calls per minute per organization on top of your API key''s own limits; exceeding it returns a data-wrapped 429 carrying no Retry-After header. Token format is `fcms_`; `api_endpoint` is the ready-to-call posts URL that embeds it. OPTIONS is supported for CORS preflight (204, `Access-Control-Allow-Methods: GET, OPTIONS`).'
      tags:
      - Sites
      security:
      - ApiKeyAuth: []
      parameters:
      - name: siteId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID. Must be a UUID and must be owned by your organization.
      responses:
        '200':
          description: 'The live delivery token, or the revoked marker. Sent with `Cache-Control: no-store`.'
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always `no-store` — the body carries a live credential and must not be cached by a browser, CDN, or proxy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                    - type: object
                      title: Active token
                      properties:
                        token:
                          type: string
                          description: 'The delivery token, `fcms_` + base64url of `siteId:version:hmac`. Re-derivable: fetching again returns the same string until a rotate or revoke.'
                          example: fcms_ZXhhbXBsZTox...
                        version:
                          type: integer
                          description: The site's current delivery-token version. Incremented by every rotation; tokens carrying an older version stop verifying.
                        api_endpoint:
                          type: string
                          description: 'Ready-to-call posts endpoint with the token embedded: `<app base URL>/api/cms/<token>/posts`.'
                        revoked:
                          type: boolean
                          enum:
                          - false
                    - type: object
                      title: Revoked
                      description: 'Terminal state: the site''s secret was nulled. No token, version, or endpoint is returned and this endpoint will not re-mint one — rotate to re-issue.'
                      properties:
                        revoked:
                          type: boolean
                          enum:
                          - true
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: FraseCMS is not enabled for this site (`cms_disabled`). Enable the FraseCMS connection first; nothing is minted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: validation_error
                  code: cms_disabled
                  message: CMS not enabled for this site
                  doc_url: https://docs.frase.io/api/errors#cms_disabled
                request_id: req_abc123
        '429':
          description: 'Rate limited. Two shapes: the standard API-key limit (Stripe-style error envelope, with Retry-After), or this endpoint''s org-scoped mint bucket of 60/minute, which is data-wrapped and carries NO Retry-After.'
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/Error'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        error:
                          type: string
                          example: Rate limit exceeded. Try again later.
        '500':
          description: The token could not be read. Data-wrapped, with a fixed message (`Failed to get delivery token`) — v1 routes never leak the underlying error. Nothing was changed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      error:
                        type: string
                        example: Failed to get delivery token
  /sites/{siteId}/cms-token/rotate:
    post:
      operationId: rotateSiteCmsDeliveryToken
      summary: Rotate a site's FraseCMS delivery token
      description: 'Mints a brand-new headless delivery token for the site and returns it. Destructive and IRREVERSIBLE: a fresh random per-site secret is generated and the site''s delivery-token version is incremented in a single update, so EVERY previously issued token for this site stops verifying the moment this call succeeds. There is no un-rotate — update every consumer with the returned token. The new token itself is not lost if you miss the response: while it is live you can read the same string back from GET /sites/{siteId}/cms-token (it is re-derived from the stored secret). What is gone forever is the old token. Gates: requires the `write:content` scope (note the sibling GET requires `write:sites` instead). Takes no request body — anything sent is ignored. An org-scoped bucket allows 10 rotations per hour per organization; exceeding it returns a data-wrapped 429 with no Retry-After header. Unlike the GET, this response does NOT set Cache-Control: no-store even though it carries a live credential — do not cache or log it. Success is recorded in analytics as a rotation for the site. OPTIONS is supported for CORS preflight (204, `Access-Control-Allow-Methods: POST, OPTIONS`).'
      tags:
      - Sites
      security:
      - ApiKeyAuth: []
      parameters:
      - name: siteId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID. Must be a UUID and must be owned by your organization.
      responses:
        '200':
          description: Rotated. The old token is already invalid.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      token:
                        type: string
                        description: The new delivery token, `fcms_` + base64url of `siteId:version:hmac`. Use it as the path credential on the FraseCMS read endpoints (`/api/cms/<token>/posts`); the rotate response does not build that URL for you — GET /sites/{siteId}/cms-token returns it as `api_endpoint`.
                        example: fcms_ZXhhbXBsZToy...
                      version:
                        type: integer
                        description: The site's new delivery-token version, one higher than before. Tokens carrying any earlier version no longer verify.
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The site is in Trash (`site_deleted`). Terminal and client-actionable — restore the site before rotating; retrying will not clear it. Data-wrapped, and unusually carries a machine-readable `code` alongside the message.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      error:
                        type: string
                        example: This site has been deleted. Restore it from Trash to keep configuring it.
                      code:
                        type: string
                        enum:
                        - site_deleted
        '429':
          description: 'Rate limited. Two shapes: the standard API-key limit (Stripe-style error envelope, with Retry-After), or this endpoint''s org-scoped rotation bucket of 10/hour, which is data-wrapped and carries NO Retry-After. A refused rotation changes nothing.'
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/Error'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        error:
                          type: string
                          example: Rate limit exceeded. Try again later.
        '500':
          description: The rotation failed. Data-wrapped, with a fixed message (`Failed to rotate delivery token`) — v1 routes never leak the underlying error. Re-read the token with GET /sites/{siteId}/cms-token to confirm which version is live before retrying.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      error:
                        type: string
                        example: Failed to rotate delivery token
  /sites:
    get:
      operationId: listSites
      summary: List sites
      description: Retrieve all websites associated with your organization.
      tags:
      - Sites
      security:
      - ApiKeyAuth: []
      parameters:
      - name: page
        in: query
        schema:
          type: integer
          default: 1
      - name: page_size
        in: query
        schema:
          type: integer
          default: 20
          maximum: 100
      responses:
        '200':
          description: List of sites
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSites'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Rate limited, or the org's monthly brief limit blocked minting the refreshed brief (only the FIRST supersession of a brief is billing-exempt; repeats meter normally)
    post:
      operationId: createSite
      summary: Create site
      description: Create a new website to track and manage content for.
      tags:
      - Sites
      security:
      - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSiteInput'
      responses:
        '201':
          description: Site created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteCreated'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Rate limited, or the org's monthly brief limit blocked minting the refreshed brief (only the FIRST supersession of a brief is billing-exempt; repeats meter normally)
  /sites/{siteId}/cms-connections:
    get:
      operationId: listCmsConnections
      summary: List CMS connections
      description: List the connection status of all supported CMS platforms (Frase CMS, WordPress, Webflow, Sanity) for a given site.
      tags:
      - Sites
      parameters:
      - name: siteId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID
      responses:
        '200':
          description: CMS connection statuses
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      connections:
                        type: array
                        items:
                          type: object
                          properties:
                            platform:
                              type: string
                              enum:
                              - frase_cms
                              - wordpress
                              - webflow
                              - sanity
                            connected:
                              type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    SiteCreated:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type: string
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        request_id:
          type: string
    PaginatedSites:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Site'
        pagination:
          $ref: '#/components/schemas/Pagination'
        request_id:
          type: string
    Site:
      type: object
      properties:
        id:
          type: string
          format: uuid
        domain:
          type: string
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
            code:
              type: string
            message:
              type: string
            param:
              type: string
            doc_url:
              type: string
        request_id:
          type: string
    CreateSiteInput:
      type: object
      required:
      - name
      - domain
      properties:
        name:
          type: string
          description: Display name for the site
        domain:
          type: string
          description: Domain name (e.g., example.com). Protocol and trailing slashes are automatically removed.
        description:
          type: string
          description: Optional description of the site
    Pagination:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer
        has_more:
          type: boolean
  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: invalid_request_error
              code: resource_not_found
              message: The requested resource was not found.
              doc_url: https://docs.frase.io/api/errors#resource_not_found
            request_id: req_abc123
    Unauthorized:
      description: Authentication failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: authentication_error
              code: invalid_api_key
              message: Invalid API key. Check that you are using the correct key.
              doc_url: https://docs.frase.io/api/errors#invalid_api_key
            request_id: req_abc123
    Forbidden:
      description: Forbidden - action not allowed on this resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: invalid_request_error
              code: forbidden
              message: System rules cannot be modified.
              doc_url: https://docs.frase.io/api/errors#forbidden
            request_id: req_abc123
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: invalid_request_error
              code: validation_error
              message: The request body is invalid.
              param: prompt_text
              doc_url: https://docs.frase.io/api/errors#validation_error
            request_id: req_abc123
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: 'API key for authentication. Get your key from Settings > API Keys. You may also send the same key as `Authorization: Bearer <key>` — see BearerAuth.'
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Frase API key (sk_live_* / sk_test_*) or MCP access token (mcp_at_*)
      description: 'Send either your API key or an MCP access token as `Authorization: Bearer <token>`. An API key sent this way is exactly equivalent to sending it in X-API-KEY — same identity, same scopes, same rate limits — so use whichever your HTTP client makes natural. X-API-KEY takes precedence if you somehow send both. Cookie/session auth is intentionally NOT supported on /api/v1/* endpoints.'