Draft a piece of marketing content and file it in the CMS as a draft.
Draft a piece of marketing content and file it in the CMS as a draft.
POST /v1/content/generate
| Address | https://api.hanzo.ai/v1/content/generate |
| Method | POST |
| Operation | post_content_generate |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Draft a piece of marketing content and file it in the CMS as a draft.
Answers 201 with the created draft's identity — {doctype, name, status} — and the document itself lands in the CMS through the SAME validate and lifecycle-hook pipeline an ordinary create runs. This is a WRITE, not a preview: there is no dry-run, and every call that succeeds leaves a document behind.
doctype picks which of two generation planes runs, and they are the only two.
Campaign and SocialPost are drafted as brand COPY on the platform AI plane (zen5 by
default, overridable per request with model or per deployment); Asset is a studio
image render the AI plane never sees. Everything else about the call is identical.
MONEY, metered in exactly one place per mode and never both. Copy rides the
platform's own inference meter — the org's balance is authorised before the model
call and debited at the exact token cost after — so content never re-bills it. A
studio render is invisible to that meter, so content is the sole meter for it: the
org is gated BEFORE the GPU compute and refused 402 when out of funds or over its
spend cap, and the debit is recorded only once the render actually returns, because
the billable event is the consumed compute and not the CMS row. project rides the
BODY rather than a server-minted identity claim, so it attributes spend but a
project-scoped cap stays soft on it — the org is the value that is enforced.
The org is the caller's own, resolved once from the validated principal and never read from the body; a caller without one is refused 403. Status is not the generator's to choose: a generated item is ALWAYS a draft, and the storage-boundary hook enforces that a second time.
It fails closed rather than inventing anything. An unknown content type is 404 and a
deployment whose marketing module is not installed is 409 naming the install call.
An AI plane or studio that is unconfigured or unreachable, a graph the studio
rejects, and a render that does not return in time all degrade to 503 — never
fabricated copy, never a fake render. A source_media that fails the SSRF and
traversal validator is 400 raised before the billing gate and before the studio is
contacted, so a hostile source never costs the caller anything.
Request
12 fields, body application/json (required).
| Field | In | Type | Required | Description |
|---|---|---|---|---|
brief | body | string | — | the brief/goal driving copy generation |
channels | body | string | — | target channels (SocialPost) |
design | body | string | — | studio design slug (asset source) |
doctype | body | string | — | Campaign | SocialPost | Asset |
kind | body | string | — | asset kind: ecom|product|lifestyle|hover|hero |
model | body | string | — | optional zen model override (copy) |
product | body | string | — | commerce product handle (copy context) |
project | body | string | — | brand/site sub-scope (billing + tenancy axis) |
source_media | body | string | — | asset source image (design CAD/photo) |
title | body | string | — | optional explicit title |
tone | body | string | — | tone override for a single draft |
voice | body | string | — | brand-voice guidance for the copy director |
Response
| Status | Body | Meaning |
|---|---|---|
201 | GenerateResult | created |
402 | GenerateResult | payment required |
201 body — 3 fields.
| Field | In | Type | Always | Description |
|---|---|---|---|---|
doctype | body | string | — | the marketing type the draft was filed as |
name | body | string | — | the new document's name — its address for every later call |
status | body | string | — | always "draft"; the lifecycle owns the initial state |
Failure carries the platform error shape — see Errors.
Examples
hanzo content generateimport { Configuration, ContentApi } from 'hanzoai';
const api = new ContentApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postContentGenerate({ brief: "<brief>", channels: "<channels>" });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ContentApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ContentApi(client).post_content_generate(brief="<brief>", channels="<channels>")cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.ContentAPI.PostContentGenerate(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, content_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = content_api::post_content_generate(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ContentApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new ContentApi(client).postContentGenerate();curl -X POST https://api.hanzo.ai/v1/content/generate \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brief": "<brief>",
"channels": "<channels>"
}'The door reaches content through the content tool, which names its 6 operations with its own verbs — this one among them, under a name only the door declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_content_board"
}
}
}'How is this guide?