---
title: "Create (or regenerate) an atomization"
method: POST
path: "/atomizations"
tags: ["Atomizations"]
---

# Create (or regenerate) an atomization

`POST /atomizations`

## Request

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

Turns a published article into one channel-native asset. Requires the `write:atomize` scope.

The handler runs its checks in this order: body validation first (so a malformed request always gets its 400 rather than a 403/429), then the plan gate (Omni-Channel Atomization requires the `professional` plan or higher), then two rate limiters (20/hour per organization+user, 50/hour per organization), then billing.

Billing charges one content credit per (organization, content, billing period), so a second type for the same article inside the same period is free by the ledger's unique claim, and a new period charges again. If the charge crosses the organization into paid per-article overage the response body sets `overage_incurred` to true and the `X-Frase-Overage-Incurred: true` header is added.

Generation is asynchronous: the row is created (or an existing row for the same content+type is reset) to `generating`, an `atomization/create` background event is dispatched, and 201 is returned immediately. Poll `GET /atomizations/{id}` for the finished payload. If a row for that content+type is already `generating`, the existing row is returned as-is with 201 — no charge, no second dispatch, and `overage_incurred` is false.

If the background dispatch itself fails, the row is released back to `draft` so the (already charged) caller can retry; the retry is free within the same period.

## Request body

- object
  - `content_id` string, uuid, required: Required. The content item to atomize. Must be a UUID belonging to your organization.
  - `type` 'linkedin_carousel' | 'twitter_thread' | 'newsletter' | 'instagram_carousel', required: Required. The channel format to produce. The route only checks that this is a string; the enum is enforced one layer down and an unknown value comes back as a 400 with `param: "input"`.

## Response `201`

Atomization queued (or an already in-flight one returned unchanged)

- object
  - `data` object
    - `id` string, uuid
    - `content_id` string, uuid
    - `type` 'linkedin_carousel' | 'twitter_thread' | 'newsletter' | 'instagram_carousel'
    - `status` 'generating': Always `generating` on this path — generation runs in the background.
    - `created_at` string, date-time: Creation time of the row. For a regenerated or already in-flight row this is the ORIGINAL creation time, not the time of this call.
    - `overage_incurred` boolean: True when this call crossed the organization into paid per-article overage. Always false on the already-in-flight path, which does not charge.

## Other responses

- `400`: Validation failed
- `401`: Authentication failed
- `403`: `plan_upgrade_required` — the organization's plan does not include Omni-Channel Atomization (professional or higher); `subscription_inactive` — the subscription is canceled, paused, past due beyond grace, or the trial has expired; or an auth-layer refusal (`insufficient_scope` when the key lacks `write:atomize`, `ip_not_allowed`).
- `404`: The content does not exist or belongs to another organization. Note this path returns the plain `{ "data": { "error": ... } }` envelope, not the structured error object.
- `429`: One of three refusals, told apart by `error.code`: `rate_limit_exceeded` (20/hour per organization+user or 50/hour per organization — carries `Retry-After` plus the `X-RateLimit-*` bag), `article_limit_exceeded` (the organization's monthly content allowance is used up — `Retry-After` only when the reset instant is known), or `site_article_budget_exceeded` (a per-site article budget is exhausted; no organization-level overage or upgrade lifts it, only a site admin raising the budget). The API-key tier limiter and monthly budget in the auth layer can also produce a 429 before the handler runs.
- `500`: Unhandled failure, including a malformed/non-JSON request body (the body is parsed inside the try block, so a parse failure is not a 400).

---

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