---
title: "Extract a reusable template from a brief or an article"
method: POST
path: "/templates/extract"
tags: ["Templates"]
---

# Extract a reusable template from a brief or an article

`POST /templates/extract`

## Request

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

Derives a template from something you already have and saves it. Requires the `write:content` scope.

With `source_type: "brief"` the brief's outline becomes the template sections and the brief's word count, language, country and resolved tone become the field defaults. With `source_type: "content"` the article's own brief outline is preferred when it has one; otherwise the HTML (or Markdown) headings of the body are parsed into sections with per-section word counts, falling back to a single `Main Content` section when the body has no headings. Both lookups are organization-scoped and skip soft-deleted rows. Extraction never produces boilerplate blocks — `boilerplate_blocks` always comes back empty.

This endpoint writes: on success the extracted template is created for your organization (a `Template from {topic or title}` name unless you pass `name`), so 201 means a new template exists and `data.id` is its id. The saved rows are clamped to the content type's allowed word-count range, while the `template` echoed in the response is the pre-clamp extraction — re-read the template via `GET /templates/{id}` if you need the stored values.

One special case returns 200 instead of 201: if the source yields no outline sections, nothing is saved and the response carries `saved: false` plus a `message` explaining that sections must be added manually.

## Request body

- object
  - `source_type` 'brief' | 'content', required: Required. Whether `source_id` names a brief or a content item.
  - `source_id` string, uuid, required: Required. The brief or content item to extract from. Must be a UUID.
  - `name` string: Optional name for the saved template. Truncated to 255 characters. Defaults to the generated `Template from ...` name. Ignored on the unsaved (no sections) path.

## Response `200`

Extracted but NOT saved — the source produced no outline sections

- object
  - `data` object
    - `template` object
      - `name` string
      - `description` string
      - `content_type` string, nullable
      - `outline_sections` TemplateSectionDef[]: Always empty on this path.
        - `heading` string, required: The section heading text
        - `level` integer, required: Heading level (1-6)
        - `keyPoints` string[]: Key points to cover in this section
        - `suggestedWordCount` integer: Suggested word count for this section
        - `isRequired` boolean: Whether this section is required
        - `generationMode` 'ai' | 'manual': Whether AI generates this section or user provides content
      - `boilerplate_blocks` BoilerplateBlock[]
        - `id` string, required: Unique identifier for the block
        - `position` 'before_section' | 'after_section' | 'header' | 'footer', required: Where to insert the block
        - `anchorSectionHeading` string: Section heading to anchor to (for before_section/after_section positions)
        - `content` string, required: The boilerplate content (max 50,000 characters)
        - `label` string, required: Display label for the block
      - `field_defaults` TemplateFieldDefaults: Default field values applied when the template is used.
        - `targetWordCount` integer: Default target word count
        - `metaTitleFormat` string: Default meta title format
        - `suggestedTone` string: Default suggested tone
        - `targetLanguage` string: Default target language
        - `targetCountry` string: Default target country
      - `source_type` 'from_brief' | 'from_content'
      - `source_content_id` string, uuid, nullable
    - `saved` false
    - `message` string

## Other responses

- `201`: Template extracted and saved
- `400`: Either a request-validation failure (`invalid_param` on `source_type` / `source_id`, in the structured error envelope) or a save refusal, which uses the plain `{ "data": { "error": ... } }` envelope: an empty (whitespace-only) name, a name that collides with an existing template in your organization (`A template with this name already exists`), an invalid heading-level sequence, or your plan's custom-template limit being reached. (An over-long `name` cannot occur - the route truncates to 255 before the check. A non-string `name` is a 500, not a 400.)
- `401`: Authentication failed
- `403`: The API key lacks the `write:content` scope (`insufficient_scope`), the caller's IP is not allow-listed (`ip_not_allowed`), or the organization's subscription/trial is no longer active.
- `404`: The brief or content item does not exist, is soft-deleted, or belongs to another organization. Returned in the plain `{ "data": { "error": ... } }` envelope, not the structured error object.
- `429`: Rate limit exceeded
- `500`: Unhandled failure, including a malformed/non-JSON request body.

---

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