# Content

URL: https://docs.hanzo.ai/docs/openapi/content

> Marketing content from draft to published, on every channel.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/content) · [MCP](https://docs.hanzo.ai/docs/mcp-tools/content) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Marketing content from draft to published, on every channel.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 6                                      |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1116 · Content — The Marketing Loop** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1116-content.md)

`/v1/content` is the marketing content loop: draft with the model and the
studio, move an item through one lifecycle, publish it to the brand's connected
channels, and read the queue board across doctypes. It is implemented in
`hanzoai/cloud` `apps/content` as a stateless orchestrator: the framework
module `marketing` holds the documents, this capability adds only what a
generic document engine cannot be — the state machine, the generation edge, the
distribution edge (`apps/content/content.go:21-41`).

### Motivation

CRUD, tenancy, permissions and install are the framework's generic surface;
duplicating them here would be a second document engine to keep honest. What
the generic engine cannot know is that a marketing item has exactly one legal
lifecycle, that a draft can be generated, and that "published" means external
side effects happened — those three are this capability.

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted
as in RFC 2119.

#### §1 It owns no store

Documents live in the framework module `marketing`; this subsystem opens no
store of its own (`apps/content/content.go:40-41`). Its state is the swappable
edges — generator, distributor, storefront (`apps/content/content.go:47-52`).

#### §2 The lifecycle is enforced at the storage boundary

For every publishable doctype, `before_save` hooks enforce two rules no matter
who writes — the `/v1/content` endpoints, a raw framework PUT, the console's
generic renderer, or an automations flow step
(`apps/content/hooks.go:13-26`):

1. status-edge legality — the one state machine in `lifecycle.go`;
2. server-owned fields (`external_ids`, `published_at`) may only be written by
   a trusted in-process op, never by a client.

Both are pure gates: a refused write is 422, and distribution stays in the
orchestrator, never in the engine.

#### §3 The addresses, and the one implementation

`GET /v1/content/lifecycle`, `GET /v1/content/board`,
`GET /v1/content/channels`, `POST /v1/content/generate`,
`POST /v1/content/publish`, and
`POST /v1/content/{doctype}/{name}/transition` — all typed
(`apps/content/typed_wire_test.go`). Every exported op is called both by the
handler and by the automations connector, so a human console, a flow, an MCP
tool call and a headless bot drive the same single implementation
(`apps/content/content.go:36-39`). Publish is idempotent per channel and
answers `in_progress` having posted nothing when it loses the per-item lease to
a live publisher (`apps/content/zipdoc_gen.go:71`).

#### §4 Tenancy and money

Every handler resolves its tenant through `principal.Org` (HIP-0026) and scopes
strictly to it (`apps/content/content.go:34-35`). The capability is metered
(`plugin/content/main.go:21`), and the meter is split on who can attribute the
work: text drafting rides the platform AI plane — identity travels on the
ChatRequest, so the one inference meter bills the tokens
(`apps/content/generate.go:27-31`) — while studio renders are GPU work the AI
plane never sees, so content is their sole meter: `Bill.Gate` before the render
(fail-closed 402) and the debit recorded after
(`apps/content/studio_render.go:121-123,172`). The metered-apps registry
carries `content` for exactly that render fee.

#### §5 Events, telemetry, stage, upstreams

It publishes no events on the platform bus; a publish's external side effects
are posts to the org's own connected channels through the distributor edge. It
emits nothing to observability beyond the request span — inference telemetry
belongs to the AI plane that serves it. Its stage is `beta`: a vertical
application. It derives from no upstream; the document engine is our own
framework module.

### Rationale

Enforcing the lifecycle in `before_save` hooks rather than in the handlers is
the load-bearing choice: the framework surface is generic and open, so a rule
enforced only in this package's handlers is a rule any other writer skips. At
the storage boundary there is no other writer. The cost is that the hooks must
stay pure — side effects in a gate would fire on every writer too — which is
why distribution lives in the orchestrator.

### Security Considerations

The wrong implementation lets a client write `external_ids` or `published_at`
directly — forging the record that something was published, or aiming a
re-publish at someone else's external post ids. The server-owned-fields gate
exists for that. The render meter is the second exposure: GPU work billed to
nobody is free compute, so the gate runs before the render and fails closed on
a frozen or broke org. Tenancy rides the validated principal on every path,
including the connector-driven ones.

## Four surfaces

| Surface  | Reaches this capability as                      | Coverage                                                                  |
| -------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
| **REST** | `content` at its own prefix                     | 6 operations                                                              |
| **CLI**  | `hanzo content …`                               | 6 of 6                                                                    |
| **SDK**  | `ContentApi` in every published client          | 6 methods                                                                 |
| **MCP**  | tool `content` on `https://api.hanzo.ai/v1/mcp` | 6 operations, 2 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/content/board`, operation `get_content_board`:

**CLI**

```bash
hanzo content board
```

**SDK**

**.ts**

```ts
import { Configuration, ContentApi } from 'hanzoai';

const api = new ContentApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getContentBoard();
```

**.py**

```python
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).get_content_board()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.ContentAPI.GetContentBoard(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::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::get_content_board(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ContentApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new ContentApi(client).getContentBoard();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/content/board \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

Tool `content`, op `get_content_board` — POST the JSON-RPC envelope to `https://api.hanzo.ai/v1/mcp`.

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "content",
         "arguments": {
           "op": "get_content_board",
           "input": {}
         }
       }
     }'
```

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                                                                | What it does                                                                                                                                                                                |
| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`POST /v1/content/{doctype}/{name}/transition`](https://docs.hanzo.ai/docs/openapi/content/post-content-by-doctype-by-name-transition) | Moves one content item to a new lifecycle state and, on the move to published, fans it out to the item's channels.                                                                          |
| [`GET /v1/content/board`](https://docs.hanzo.ai/docs/openapi/content/get-content-board)                                                 | Aggregates the caller org's marketing content across every publishable content type into ONE queue board — the cross-type read the framework's per-DocType list cannot give.                |
| [`GET /v1/content/channels`](https://docs.hanzo.ai/docs/openapi/content/get-content-channels)                                           | Lists the distribution channels the caller's org has connected — the social integrations a publish can target.                                                                              |
| [`POST /v1/content/generate`](https://docs.hanzo.ai/docs/openapi/content/post-content-generate)                                         | Draft a provider of marketing content and file it in the CMS as a draft.                                                                                                                    |
| [`GET /v1/content/lifecycle`](https://docs.hanzo.ai/docs/openapi/content/get-content-lifecycle)                                         | Returns the ONE marketing-content state machine: the ordered lifecycle states, which state a fresh document starts in, which one is publicly live, and the legal successors of every state. |
| [`POST /v1/content/publish`](https://docs.hanzo.ai/docs/openapi/content/post-content-publish)                                           | Distributes one CMS content item to the channels recorded on it and returns the honest per-channel outcome.                                                                                 |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
