---
title: "Bulk import CMS posts"
method: POST
path: "/cms/posts/import"
tags: ["CMS"]
---

# Bulk import CMS posts

`POST /cms/posts/import`

## Request

- Base URL: `https://next.frase.io/api/v1`
- URL: `https://next.frase.io/api/v1/cms/posts/import`
- Auth: one of:
  - API key in header `X-API-KEY`
  - HTTP bearer

Start an asynchronous bulk import of up to 100 posts. Returns a job ID that can be polled for progress. CMS must be enabled on the target site.

## Request body

- object
  - `site_id` string, uuid, required
  - `posts` object[], required
    - `title` string, required
    - `content_format` 'html' | 'portable_text': How `content` is encoded. Default `html`. When `portable_text`, `content` is a standard Portable Text blocks array; the server converts it to HTML before the 500KB cap and the sanitize/rehost pipeline. Unknown block types are skipped and logged.
    - `content` union, required: Post body. An HTML string when content_format is `html` (default), or a Portable Text blocks array when `portable_text`. The 500KB cap applies to the resulting HTML.
      - string
      - object[]
    - `slug` string
    - `status` 'draft' | 'published' | 'scheduled'
    - `type` 'blog_post' | 'page'
    - `excerpt` string
    - `featured_image` string, uri
    - `seo_title` string
    - `seo_description` string
    - `focus_keyword` string
    - `canonical_url` string, uri
    - `source_url` string: The post's URL on its origin site (the old URL before migration). Unlike `canonical_url`, this may be an http(s) absolute URL or a site-relative path. The import registers a 301 redirect from this URL's normalized path to the new post, so existing bookmarks and backlinks keep working; the value is also stored on the post for later internal-link rewriting. Skipped for `page`-type posts (root-routed).
    - `published_at` string, date-time
    - `updated_at` string, date-time: Preserved as the post's dateModified instead of being stamped to the import time. Must not be earlier than `published_at`.
    - `schema_markup` object: JSON-LD structured data for the post. Must include `@context` and `@type`. Additional properties depend on the chosen schema type. See [schema.org](https://schema.org) for full type definitions.
      - `@context` string: Schema.org context URL. Almost always `https://schema.org`.
      - `@type` 'Article' | 'BlogPosting' | 'NewsArticle' | 'FAQPage' | 'HowTo' | 'TechArticle' | 'WebPage': The schema.org type for this content.
      - `headline` string: The headline of the article. Recommended for Article, BlogPosting, NewsArticle.
      - `description` string: A short description or summary of the content.
      - `author` union
        - object: Author as a Person or Organization.
          - `@type` 'Person' | 'Organization'
          - `name` string
          - `url` string, uri
        - object[]: Multiple authors.
          - `@type` 'Person' | 'Organization'
          - `name` string
          - `url` string, uri
      - `datePublished` string, date: Original publication date in ISO 8601 format (e.g., `2026-04-16`).
      - `dateModified` string, date: Last modification date in ISO 8601 format.
      - `image` union: Primary image. Can be a URL string or an ImageObject.
        - string, uri
        - object
          - `@type` 'ImageObject'
          - `url` string, uri
          - `width` integer
          - `height` integer
      - `publisher` object: The publisher of the content.
        - `@type` 'Organization'
        - `name` string
        - `logo` object
          - `@type` 'ImageObject'
          - `url` string, uri
      - `mainEntity` union: Primary entity of the page. Used by FAQPage (array of Question) and HowTo (HowToStep array).
        - object[]: For FAQPage: array of Question objects.
          - `@type` 'Question'
          - `name` string: The question text.
          - `acceptedAnswer` object
            - `@type` 'Answer'
            - `text` string: The answer text.
        - object
      - `step` object[]: For HowTo: ordered list of steps.
        - `@type` 'HowToStep'
        - `name` string: Step title.
        - `text` string: Step instructions.
        - `image` string, uri
    - `meta` object
    - `related_slugs` string[]: Ordered curated related-post slugs (max 3), resolved to ids in a final pass once every post in the import exists. SETS the relation for published targets on the same site; an empty array is a no-op (use PUT /cms/posts/{id} with related_post_ids: [] to clear). Unresolvable, draft, or cross-site slugs are skipped with warnings.
  - `options` object
    - `on_conflict` 'skip' | 'overwrite': Behavior when a post with the same slug already exists
    - `default_status` 'draft' | 'published' | 'scheduled': Default status for posts that don't specify one

## Response `202`

Import job queued

- object
  - `data` object
    - `job_id` string, uuid
    - `status` 'queued'
    - `posts_queued` integer
    - `site_id` string, uuid

## Other responses

- `401`: Authentication failed
- `403`: Forbidden - action not allowed on this resource
- `404`: Resource not found
- `422`: Validation failed
- `429`: Rate limit exceeded

---

[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)
