---
title: "Dry-run a CMS post create"
method: POST
path: "/cms/posts/validate"
tags: ["CMS"]
---

# Dry-run a CMS post create

`POST /cms/posts/validate`

## Request

- Base URL: `https://next.frase.io/api/v1`
- URL: `https://next.frase.io/api/v1/cms/posts/validate`
- Auth: API key in header `X-API-KEY`

Rehearse `POST /cms/posts` without writing anything. Runs the same structural gates the create path runs — required fields and length caps, scalar shape checks (status enum, HTTPS URLs, ISO 8601 `published_at`), site ownership plus the CMS-enabled and per-site CMS feature gates, the content-type enum and its plan gate, the robots enum, `content_type_config` object/size/per-type schema, slug availability, and whether sanitization would alter your HTML. No rows are written and no images are fetched or rehosted. Requires the `write:content` scope because it rehearses a write and reveals plan and feature-gate state. `valid: true` means every structural gate would pass; taxonomy (authors, categories, tags, FAQs) and `related_post_ids` are not checked here, so a create can still reject a malformed taxonomy payload. Site gates and the slug preview only run once the site is confirmed to belong to your organization, so this endpoint cannot probe another organization's slugs or entitlements — an unowned or unknown `site_id` simply comes back as a `site_id` error rather than a 404. It also returns a content-rules preflight (`rules`) using the same evaluation the publish gate applies, so you can see a block coming before writing; `rules` is advisory and never affects `valid`, and it is `null` when `site_id` or `content` was not a string, or when the preflight itself failed.

## Request body

- object
  - `site_id` string, uuid: Must be a valid UUID owned by your organization, or it is reported as a `site_id` error.
  - `title` string
  - `content` string: HTML body. Also used for the sanitization check and the rules preflight.
  - `type` string: Canonical content type. Checked against the content-type taxonomy and your plan's allowed CMS types.
  - `status` 'draft' | 'published' | 'scheduled'
  - `slug` string: Used as the slug base; falls back to the title when omitted.
  - `robots` 'index,follow' | 'noindex,follow' | 'noindex,nofollow' | 'index,nofollow'
  - `excerpt` string
  - `seo_title` string
  - `seo_description` string
  - `featured_image` string, uri: Must be HTTPS.
  - `canonical_url` string, uri: Must be HTTPS.
  - `og_image` string, uri: Must be HTTPS.
  - `og_title` string
  - `published_at` string, date-time
  - `content_type_config` object: Per-content-type configuration object. Must be a JSON object (not an array), at most 65536 bytes serialized, and must parse against the schema registered for the resolved type.

## Response `200`

Validation ran. Read `valid` for the verdict — a 200 does not mean the input is acceptable.

- object
  - `data` object
    - `valid` boolean: True when `errors` is empty, i.e. a create would pass every structural gate.
    - `errors` object[]: Every structural failure, collected in one pass.
      - `field` string: The offending request field, in wire (snake_case) form.
      - `message` string
    - `warnings` string[]: Non-blocking notes — a taken slug that would be suffixed, or markup that sanitization would strip.
    - `normalized_slug` string, nullable: The slug a create would use (base slug plus a uniqueness suffix if taken). Null when there is no title or slug, or the site is not owned. A concurrent create could still force a different suffix.
    - `sanitized` boolean: True when sanitization would strip or alter the submitted HTML.
    - `rules` object, nullable: Content-rules preflight. Null when `site_id` or `content` was not a string, or the preflight could not run. Advisory only.
      - `overall_status` 'pass' | 'fail' | 'warning': Worst status across every enabled rule set that applies.
      - `would_block_publish` boolean: True when `overall_status` is `fail`; a publish would refuse (a draft create still succeeds).
      - `needs_force` boolean: True when `overall_status` is `warning`.
      - `no_rules_configured` boolean: True when no enabled rule set applied at all — a `pass` here means governance was never configured, not that the content cleared any check.
      - `site_scoped_sets_skipped` integer: Enabled site-scoped rule sets excluded because no site was in scope. On THIS endpoint the preflight only runs when site_id is present, so the value is always 0 - the field exists for shape parity with the shared evaluator.
      - `violations` object[]: Every rule that did not pass, flattened across rule sets.
        - `rule_name` string
        - `severity` 'error' | 'warning' | 'info'
        - `message` string

## Other responses

- `400`: Validation failed
- `401`: Authentication failed
- `403`: Forbidden - action not allowed on this resource
- `429`: Rate limit exceeded
- `500`: The validator itself faulted (the input alone can never cause this).

---

[API](https://skmtc.dev/frase/apis/frase-api.md) · [All operations](https://skmtc.dev/frase/apis/frase-api/llms.txt) · [OpenAPI document](https://skmtc.dev/frase/apis/frase-api/revisions/7fb75d89f7a4?raw)
