Frase CMS Delivery (Public) API

The CMS Delivery (Public) API from Frase — 2 operation(s) for cms delivery (public).

GitHub OpenAPI

Operations 2

GET /cms/{deliveryToken}/posts List published posts (headless delivery) #
GET /cms/{deliveryToken}/posts/{slug} Get a published post by slug (headless delivery) #

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-cms-delivery-public-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-cms-delivery-public-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Frase CMS Delivery (Public) 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: CMS Delivery (Public)
paths:
  /cms/{deliveryToken}/posts:
    servers:
    - url: https://next.frase.io/api
      description: Production
    - url: http://localhost:3000/api
      description: Local Development
    get:
      operationId: listDeliveryPosts
      summary: List published posts (headless delivery)
      description: Public, token-gated headless delivery. Returns published posts for the site identified by the fcms_ delivery token. No API key; the token in the path both identifies and authorizes the site. Rate-limited per-IP and per-site; metered against the site's monthly view allowance.
      tags:
      - CMS Delivery (Public)
      security: []
      parameters:
      - name: deliveryToken
        in: path
        required: true
        schema:
          type: string
        description: Per-site fcms_-prefixed delivery token (from CMS settings). Replaces the raw siteId in the path; identifies AND authorizes the site.
      - name: type
        in: query
        required: false
        schema:
          type: string
          enum:
          - blog_post
          - page
          default: blog_post
        description: Post type filter
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          default: 10
          maximum: 100
        description: Page size (max 100)
      - name: offset
        in: query
        required: false
        schema:
          type: integer
          default: 0
        description: Pagination offset
      - name: updated_since
        in: query
        required: false
        description: ISO 8601 instant. Returns only posts with updated_at >= this (incremental sync). Combines with type/limit/offset. A malformed value returns 400 before any DB read.
        schema:
          type: string
          format: date-time
      - name: category
        in: query
        required: false
        description: Filter published posts by category slug, site-scoped (a slug from another site never matches). Exact match only in v1 (no descendant/hierarchy inclusion). Combines with type/limit/offset/sort/updated_since/tag. An unknown or never-matching slug returns an empty posts array with 200, not a 404. Has its own ETag and metering dedup key (no cross-filter 304).
        schema:
          type: string
      - name: tag
        in: query
        required: false
        description: Filter published posts by tag slug, site-scoped (a slug from another site never matches). Combines with type/limit/offset/sort/updated_since/category. An unknown or never-matching slug returns an empty posts array with 200, not a 404. Has its own ETag and metering dedup key (no cross-filter 304).
        schema:
          type: string
      - name: If-None-Match
        in: header
        required: false
        description: Conditional GET. When it matches the response ETag, the API returns 304 Not Modified (empty body, ETag still set) and does not meter the request.
        schema:
          type: string
      responses:
        '200':
          description: Published posts. Carries an ETag (sha1 of the page's id + updated_at); send it back as If-None-Match for a 304 when nothing changed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  posts:
                    type: array
                    items:
                      $ref: '#/components/schemas/CmsDeliveryPostSummary'
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
                      hasMore:
                        type: boolean
        '400':
          description: updated_since is not a valid ISO 8601 date.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '304':
          description: Not Modified. If-None-Match matched the current ETag; empty body, ETag header still set.
          headers:
            ETag:
              description: The content-version ETag.
              schema:
                type: string
        '401':
          description: Missing or invalid fcms_ delivery token
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: invalid_delivery_token
        '402':
          description: Site is over its monthly view limit (quota block, distinct from the transient 429 rate limit)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - view_limit_exceeded
                    - spend_cap_reached
                    example: view_limit_exceeded
        '404':
          description: CMS not enabled for this site, or post not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '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)
        '503':
          description: Subscription inactive, delivery temporarily unavailable
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Service temporarily unavailable
  /cms/{deliveryToken}/posts/{slug}:
    servers:
    - url: https://next.frase.io/api
      description: Production
    - url: http://localhost:3000/api
      description: Local Development
    get:
      operationId: getDeliveryPostBySlug
      summary: Get a published post by slug (headless delivery)
      description: Public, token-gated headless delivery of a single published post by slug. Same token/rate-limit/metering model as the list endpoint.
      tags:
      - CMS Delivery (Public)
      security: []
      parameters:
      - name: deliveryToken
        in: path
        required: true
        schema:
          type: string
        description: Per-site fcms_-prefixed delivery token (from CMS settings). Replaces the raw siteId in the path; identifies AND authorizes the site.
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: Post slug
      - name: If-None-Match
        in: header
        required: false
        description: Conditional GET. When it matches the response ETag ("<id>-<updatedAt ISO>"), the API returns 304 Not Modified (empty body, ETag still set) and does not meter the request.
        schema:
          type: string
      responses:
        '200':
          description: The published post. Carries an ETag of the form "<id>-<updatedAt ISO>"; send it back as If-None-Match for a 304 when unchanged.
          content:
            application/json:
              schema:
                type: object
                properties:
                  post:
                    $ref: '#/components/schemas/CmsDeliveryPost'
        '304':
          description: Not Modified. If-None-Match matched the current ETag; empty body, ETag header still set.
          headers:
            ETag:
              description: The content-version ETag.
              schema:
                type: string
        '401':
          description: Missing or invalid fcms_ delivery token
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: invalid_delivery_token
        '402':
          description: Site is over its monthly view limit (quota block, distinct from the transient 429 rate limit)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - view_limit_exceeded
                    - spend_cap_reached
                    example: view_limit_exceeded
        '404':
          description: CMS not enabled for this site, or post not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '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)
        '503':
          description: Subscription inactive, delivery temporarily unavailable
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Service temporarily unavailable
components:
  schemas:
    CmsDeliveryPost:
      type: object
      description: A published CMS post served by the public headless-delivery single-post API. snake_case (consistent with the API-key v1 CmsPost). Includes taxonomy (primary_author/authors/category/tags) and all SEO scalars.
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        type:
          type: string
          enum:
          - blog_post
          - page
        title:
          type: string
        excerpt:
          type: string
          nullable: true
        featured_image:
          type: string
          nullable: true
          format: uri
        featured_image_alt:
          type: string
          nullable: true
        seo_title:
          type: string
          nullable: true
        seo_description:
          type: string
          nullable: true
        focus_keyword:
          type: string
          nullable: true
        canonical_url:
          type: string
          nullable: true
          format: uri
        schema_markup:
          type: object
          nullable: true
          additionalProperties: true
          description: Editor-authored JSON-LD structured data, if set.
        og_image:
          type: string
          nullable: true
          format: uri
        og_title:
          type: string
          nullable: true
        og_description:
          type: string
          nullable: true
        published_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        primary_author:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
            avatar_url:
              type: string
              nullable: true
              format: uri
            bio:
              type: string
              nullable: true
            role:
              type: string
              nullable: true
              description: e.g. "Head of Content". Set on author creation only; a re-import never overwrites an existing author's role.
          required:
          - id
          - name
          - slug
          description: Primary/canonical author, or null.
        authors:
          type: array
          description: Byline authors (incl. the primary), ordered by position.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              slug:
                type: string
              position:
                type: integer
              avatar_url:
                type: string
                nullable: true
                format: uri
              role:
                type: string
                nullable: true
                description: e.g. "Head of Content". Set on author creation only; a re-import never overwrites an existing author's role.
            required:
            - id
            - name
            - slug
            - position
        category:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
            parent_id:
              type: string
              format: uuid
              nullable: true
          required:
          - id
          - name
          - slug
          description: Single category, or null.
        tags:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              slug:
                type: string
            required:
            - id
            - name
            - slug
        content:
          type: string
          description: Rendered HTML body.
        meta:
          type: object
          nullable: true
          additionalProperties: true
        json_ld:
          type: object
          nullable: true
          additionalProperties: true
          description: 'Auto-generated schema.org JSON-LD for this post (per content type: BlogPosting/Article/FAQPage/HowTo/Review/etc.), or null when no markup applies. Ready to embed in a <script type="application/ld+json"> tag. Editor-authored schema_markup overrides the generated value.'
      required:
      - id
      - slug
      - type
      - title
      - content
    CmsDeliveryPostSummary:
      type: object
      description: A published post in the headless-delivery LIST response. snake_case (consistent with the API-key v1 CmsPost). Metadata + taxonomy + SEO scalars; the rendered body (content) and meta appear only on the single-post response.
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        type:
          type: string
          enum:
          - blog_post
          - page
        title:
          type: string
        excerpt:
          type: string
          nullable: true
        featured_image:
          type: string
          nullable: true
          format: uri
        featured_image_alt:
          type: string
          nullable: true
        seo_title:
          type: string
          nullable: true
        seo_description:
          type: string
          nullable: true
        focus_keyword:
          type: string
          nullable: true
        canonical_url:
          type: string
          nullable: true
          format: uri
        schema_markup:
          type: object
          nullable: true
          additionalProperties: true
          description: Editor-authored JSON-LD structured data, if set.
        og_image:
          type: string
          nullable: true
          format: uri
        og_title:
          type: string
          nullable: true
        og_description:
          type: string
          nullable: true
        published_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        primary_author:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
            avatar_url:
              type: string
              nullable: true
              format: uri
            bio:
              type: string
              nullable: true
            role:
              type: string
              nullable: true
              description: e.g. "Head of Content". Set on author creation only; a re-import never overwrites an existing author's role.
          required:
          - id
          - name
          - slug
          description: Primary/canonical author, or null.
        authors:
          type: array
          description: Byline authors (incl. the primary), ordered by position.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              slug:
                type: string
              position:
                type: integer
              avatar_url:
                type: string
                nullable: true
                format: uri
              role:
                type: string
                nullable: true
                description: e.g. "Head of Content". Set on author creation only; a re-import never overwrites an existing author's role.
            required:
            - id
            - name
            - slug
            - position
        category:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
            parent_id:
              type: string
              format: uuid
              nullable: true
          required:
          - id
          - name
          - slug
          description: Single category, or null.
        tags:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              slug:
                type: string
            required:
            - id
            - name
            - slug
      required:
      - id
      - slug
      - type
      - title
  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.'