---
name: subnano-publishing-api
description: Create drafts, upload images, and publish posts with Subnano Publishing API v1. Use when an agent needs to automate post publishing, debug publish failures, or run end-to-end API checks.
version: 1.1.0
updated: 2026-07-26
---

# Subnano Publishing API Skill

Use this skill when an agent must call Subnano's Publishing API directly.

## Base URLs

- Production: `https://subnano.me/api/v1`

## Authentication

Use personal API key header on every request:

`Authorization: Bearer snpk_<keyId>_<secret>`

Security rules:

- Never print full keys in logs or responses.
- Never place API keys in query parameters.

## Core Endpoints

- `POST /posts` create draft
- `GET /posts?status=draft|published&page=1&per_page=20` list posts
- `GET /posts/:id` get post by UUID
- `PATCH /posts/:id` update post fields
- `DELETE /posts/:id` delete draft
- `POST /posts/:id/images?intent=content|og` upload image
- `POST /posts/:id/publish` publish draft
- `POST /posts/:id/unpublish` unpublish post

## Agent Workflow

Run this order for a full publish flow:

1. `POST /posts` create a draft in one of these shapes:
   - `freeContentMarkdown` only for a free-only post
   - `paidContentMarkdown` only for a paid-only post with no teaser
   - both `freeContentMarkdown` and `paidContentMarkdown` for a paywalled post with a free teaser
2. Upload image(s) via `POST /posts/:id/images` if the post needs content or OG media.
3. `PATCH /posts/:id` with final `paidContentMarkdown`, `freeContentMarkdown`, and any metadata changes.
4. Ensure the draft has every required publish field, including creation disclosure regardless of paywall state.
5. `POST /posts/:id/publish` with `Idempotency-Key`.
6. `GET /posts/:id` verify status and URL.

Paywall rule:

- If `enablePaywall` is `true` but `paidContentMarkdown` is empty, the API treats `enablePaywall` as `false`.
- When `enablePaywall` is `false`, either `paidContentMarkdown` or `freeContentMarkdown` can be the freely accessible content.
- Empty `paidContentMarkdown` and `freeContentMarkdown` fields are returned as `null` in API responses.

If asked to delete a published post:

1. `POST /posts/:id/unpublish`
2. `DELETE /posts/:id`

## Required Publish Fields

Before publish can succeed, the post must have:

- non-empty `title`
- non-empty `description`
- non-empty publishable markdown:
  - `paidContentMarkdown` when `enablePaywall` is `true`
  - `paidContentMarkdown` or `freeContentMarkdown` when `enablePaywall` is `false`
- optional `freeContentMarkdown` teaser in either mode
- `priceRaw` as a positive integer string when `enablePaywall` is `true`
- valid `primaryCategoryId`
- `secondaryCategoryId` different from primary (if set)
- `creationMethod` and `creationAttested: true` for every published post

If `enablePaywall` is `false`, `priceRaw` is ignored and stored as `null`.

Creation disclosure records the author's declaration about their production process. It is not AI detection or a verified-authorship claim. Supported `creationMethod` values:

- `human_written`
- `ai_assisted_editing`
- `ai_assisted_research`
- `ai_assisted_drafting`
- `primarily_ai_generated`

`creationDetails` is optional plain text with a maximum length of 280 characters.

`creationAttested: true` records this author statement:

> I have reviewed this post and take responsibility for publishing it.

Do not set the attestation automatically. Set it only after the author has reviewed the final post and authorized publication.

## Idempotency Rules

`POST /posts/:id/publish` requires:

`Idempotency-Key: <uuid>`

Behavior:

- same key + same request => replay stored response
- same key while original request is in-flight => `409`
- same key + different fingerprint => `422`
- publish on already-published post => `200` with `publishResult: "no_op_already_published"` and `stateChanged: false`

Replay responses may include `x-idempotency-replay: true`.

## JSON Payload Templates

### Create Paid Draft (Publish-Ready)

```json
{
  "title": "Agent draft",
  "description": "Draft created via API",
  "paidContentMarkdown": "# Hello from API\n\nDraft body.",
  "enablePaywall": true,
  "priceRaw": "100000000000000000000000000000",
  "primaryCategoryId": 4,
  "language": "en",
  "commentsEnabled": true,
  "creationMethod": "human_written",
  "creationAttested": true
}
```

### Create Free Draft (Publish-Ready)

```json
{
  "title": "Free agent draft",
  "description": "Free post created via API",
  "freeContentMarkdown": "# Hello from API\n\nThis post is free to read.",
  "enablePaywall": false,
  "primaryCategoryId": 4,
  "language": "en",
  "commentsEnabled": true,
  "creationMethod": "ai_assisted_research",
  "creationDetails": "AI helped locate sources; the author reviewed and wrote the post.",
  "creationAttested": true
}
```

### Patch Content With Two Images

```json
{
  "paidContentMarkdown": "# Final title\n\nIntro text.\n\n![Image 1](https://example.com/image-1.jpg)\n\nMore text.\n\n![Image 2](https://example.com/image-2.jpg)"
}
```

### Patch OG Metadata

```json
{
  "ogTitle": "Custom social title",
  "ogDescription": "Custom social description",
  "ogImage": "https://example.com/og-image.jpg"
}
```

## Curl Recipes

Set env once:

```bash
export BASE_URL="https://subnano.me/api/v1"
export SUBNANO_PUBLISH_KEY="snpk_<keyId>_<secret>"
```

Create draft:

```bash
curl -sS -X POST "$BASE_URL/posts" \
  -H "Authorization: Bearer $SUBNANO_PUBLISH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Agent draft",
    "description": "Draft created via API",
    "paidContentMarkdown": "# Hello from API\n\nDraft body.",
    "enablePaywall": true,
    "priceRaw": "100000000000000000000000000000",
    "primaryCategoryId": 4,
    "language": "en",
    "commentsEnabled": true,
    "creationMethod": "human_written",
    "creationAttested": true
  }'
```

Upload content image:

```bash
curl -sS -X POST "$BASE_URL/posts/$POST_ID/images?intent=content" \
  -H "Authorization: Bearer $SUBNANO_PUBLISH_KEY" \
  -F "file=@/absolute/path/to/image.jpg"
```

Patch content:

```bash
curl -sS -X PATCH "$BASE_URL/posts/$POST_ID" \
  -H "Authorization: Bearer $SUBNANO_PUBLISH_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"paidContentMarkdown\":\"# Final title\\n\\n![Image](https://example.com/image.jpg)\"}"
```

Tip: avoid double-escaping newlines in shell payloads. Use `\\n` in JSON (not `\\\\n`), or use a heredoc payload.

Publish:

```bash
curl -sS -X POST "$BASE_URL/posts/$POST_ID/publish" \
  -H "Authorization: Bearer $SUBNANO_PUBLISH_KEY" \
  -H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')"
```

## Image Upload Rules

- Endpoint: `POST /posts/:id/images`
- Use query `?intent=content` or `?intent=og`
- File type must be `image/*`
- Max size:
  - `content`: 5MB
  - `og`: 2MB
- `intent=og` auto-updates OG image metadata

## Category IDs (Current)

Fetch live categories from frontend API when needed:

- Production: `GET https://subnano.me/api/categories`

Known values:

- `1` Culture
- `2` Technology
- `3` Business
- `4` U.S. Politics
- `5` Finance
- `6` Food & Drink
- `7` Sports
- `8` Art & Illustration
- `9` World Politics
- `10` Health Politics
- `11` News
- `12` Fashion & Beauty
- `13` Music
- `14` Faith & Spirituality
- `15` Climate & Environment
- `16` Science
- `17` Literature
- `18` Fiction
- `19` Health & Wellness
- `20` Design
- `21` Travel
- `22` Parenting
- `23` Philosophy
- `24` Comics
- `25` International
- `26` Crypto
- `27` History
- `28` Humor
- `29` Education
- `30` Other

## Supported Language Values

- `en`, `en-US`, `en-GB`
- `es`, `fr`, `de`, `da`, `sv`, `no`, `it`
- `pt`, `pt-BR`, `pt-PT`
- `nl`, `fi`, `pl`, `cs`, `hu`
- `ja`, `ko`, `zh-CN`, `zh-TW`

## Error Format

Errors use `application/problem+json`:

```json
{
  "type": "https://subnano.me/problems/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "One or more fields are invalid",
  "errors": [{ "field": "primaryCategoryId", "code": "required" }]
}
```

Common statuses:

- `400` malformed request or missing required headers
- `401` missing/invalid API key
- `403` key not allowed for action
- `404` post or endpoint not found
- `409` idempotency request in-flight
- `413` uploaded image too large
- `422` validation failure or idempotency mismatch
- `429` rate limit exceeded
- `5xx` transient server failure

## Expected Agent Output

When executing this skill, return:

1. Request sequence and status codes.
2. IDs/URLs generated (`postId`, uploaded image URLs, published URL).
3. Any problem JSON errors with actionable diagnosis.
4. Whether publish completed and whether replay/no-op behavior occurred.
