API Reference
Base URL: https://content-apiapi-production.up.railway.app/v1 · Auth: x-api-key: capi_xxx or Authorization: Bearer capi_xxx · Not a developer? Read the User Guide instead.
Quick Start
# 1. Generate content (returns immediately with a job_id)
curl -X POST https://content-apiapi-production.up.railway.app/v1/content/generate \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json" \
-d '{
"type": "caption",
"topic": "AI in HR: opportunity or threat?"
}'
# Response (202 Accepted)
# {"job_id": "550e8400-e29b-41d4-a716-446655440000", "estimated_seconds": 10}
# 2. Poll for result — repeat every 3-5s until status is terminal
curl https://content-apiapi-production.up.railway.app/v1/content/550e8400-e29b-41d4-a716-446655440000 \
-H "x-api-key: capi_your_key"
# Response (200 OK, when completed)
# {
# "job_id": "550e8400-e29b-41d4-a716-446655440000",
# "status": "completed",
# "content": "...",
# "quality_score": 9,
# "review_report": {"verdict": "APPROVE", "voice_score": 9}
# }Installation & Integration
Playground (Browser — No Install)
Visit /playground, sign in, and generate in the browser. No code needed. Best for testing the API before integrating.
REST API (Any HTTP Client)
Works with curl, Python, Node.js, Go — anything that speaks HTTP. Examples shown throughout this reference use curl.
MCP (Claude Desktop / VS Code / Cursor)
Add to ~/.claude/claude_desktop_config.json (Claude Desktop) or .vscode/mcp.json (VS Code):
{
"mcpServers": {
"content-api": {
"type": "streamable-http",
"url": "https://content-apiapi-production.up.railway.app/mcp",
"headers": {
"x-api-key": "capi_your_key"
}
}
}
}Restart Claude Desktop. Type write a caption about... — the write_content MCP tool runs.
MCP Tools:
| Tool | Purpose |
|---|---|
| write_content | Generate content end-to-end; polls until done (3-min cap), returns final draft + score. |
| list_voices | List your custom voice profiles (metadata only). |
| get_content_status | Check status of a running job by job_id. |
CLI (npm Global)
# Install (v0.1.0 published to npm 2026-05-17)
npm install -g @content-agent/cli
# 1. Save your API key — stored at ~/.content-agent.json (mode 0600)
content-agent auth set capi_your_key
# 2. Generate content — topic is positional, --type defaults to blog
content-agent write "AI productivity tips" --type caption
# 3. Generate with more options
content-agent write "AI in healthcare" --type article --tier pro --out ai-health.md
content-agent write "Launch v2.0" --type email --mode A --brief "B2B, friendly tone"
# 4. Check a job by ID
content-agent status <job_id>
content-agent status <job_id> --content # also prints the content
# 5. View monthly quota
content-agent usage
# 6. Show identity, plan, and usage
content-agent auth whoami
# 7. Override API base URL (self-hosted deployments)
content-agent auth url https://my-instance.example.comAvailable commands: auth (set / whoami / url), write <topic>, status <job-id>, usage. There is no list-voices command — manage voices via GET /v1/voices or the dashboard.
Auth priority: env CONTENT_AGENT_API_KEY beats the config file (useful for CI). API URL override: env CONTENT_AGENT_API_URL or content-agent auth url <url>.
Polling: write polls every 3s for up to 120s. On timeout, use content-agent status <job_id> to check manually.
Glossary — Plan vs Tier (Don't Mix Them Up)
Two concepts share the word "pro" — here is the canonical cheat-sheet:
- Plan (billing)
- What the user pays for: Free, Starter, Pro, or Enterprise. Stored in user.plan. Controls monthly quota + voice profile limits.
- Tier (pipeline)
- AI investment level passed in the tier request field: basic, upgrade, or pro. Controls model selection, checklist depth, and thinking mode. The system auto-picks based on type if you omit it.
- Status (job top-level)
- One of: pending, running, completed, rejected, failed. Only the first three count toward monthly quota.
- Verdict (inside review_report)
- One of: APPROVE, REQUEST_CHANGES, REJECT. Independent from status — a job with status=completed and verdict=REJECT still consumed quota.
- Free tier override
- When user.plan=free, the server forces tier=basic and caps max_words=300 regardless of what you request.
Authentication
Get an API Key
1. Go to /register and enter your email.
2. Click the verification link in the email.
3. Your API key is shown once — save it immediately.
Key format: capi_ + 32 random bytes (base64url).
Pass It to Requests
# Option 1: x-api-key header
curl https://... -H "x-api-key: capi_your_key"
# Option 2: Authorization bearer token
curl https://... -H "Authorization: Bearer capi_your_key"Managing Multiple Keys
Create additional keys to rotate or split by application:
POST /v1/auth/keys
curl -X POST https://content-apiapi-production.up.railway.app/v1/auth/keys \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json"
# Response 201
# {"id": "key-uuid", "key": "capi_new_key"}GET /v1/auth/keys
curl https://content-apiapi-production.up.railway.app/v1/auth/keys \
-H "x-api-key: capi_your_key"
# Response 200
# {"keys": [{"id": "key-uuid", "created_at": "...", "last_used": "..."}]}DELETE /v1/auth/keys/:id
curl -X DELETE https://content-apiapi-production.up.railway.app/v1/auth/keys/key-uuid \
-H "x-api-key: capi_your_key"
# Response 204 (no content)Content Generation
POST /v1/content/generate
Create a content generation job. Returns immediately with a job_id.
curl -X POST https://content-apiapi-production.up.railway.app/v1/content/generate \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json" \
-d '{
"type": "blog",
"topic": "Why async communication works",
"brief": "Audience: remote team leaders. Goal: convince them to adopt async. Tone: practical.",
"tier": "pro",
"voice_mode": "A",
"voice_id": null,
"webhook": null
}'
# Response 202 Accepted
# {
# "job_id": "550e8400-e29b-41d4-a716-446655440000",
# "estimated_seconds": 60
# }| Field | Type | Required | Description |
|---|---|---|---|
| type | string | ✓ | blog · article · landing · marketing · email · caption |
| topic | string | ✓ | What to write about (3–500 chars) |
| tier | string | — | basic · upgrade · pro · null (auto-detect by type) |
| voice_mode | string | — | A (story) · B (principle) · C (analysis) — default A |
| voice_id | uuid | — | Custom voice profile ID (omit for default) |
| brief | string | — | Additional context: audience, goal, CTA (max 5000 chars) |
| webhook | string (URL) | — | Receive result via webhook instead of polling |
Estimated generation times: caption ~10s · marketing ~20s · email ~30s · blog ~60s · article ~90s · landing ~75s
Errors: 400 validation · 401 bad key · 403 tier restriction (free tier: caption + marketing only; free tier can't create voices) · 429 monthly quota exhausted · 503 queue temporarily unavailable
GET /v1/content/:job_id
Poll job status. Repeat every 3–5s until status is completed, rejected, or failed.
curl https://content-apiapi-production.up.railway.app/v1/content/550e8400-e29b-41d4-a716-446655440000 \
-H "x-api-key: capi_your_key"
# Response 200
# {
# "job_id": "550e8400-e29b-41d4-a716-446655440000",
# "status": "completed",
# "content": "Full generated content text here...",
# "quality_score": 9,
# "voice_version": "abc123def456",
# "tier": "pro",
# "voice_mode": "A",
# "review_report": {
# "verdict": "APPROVE",
# "voice_score": 9,
# "issues": [],
# "patches": []
# },
# "generated_at": "2026-05-18T16:30:00.000Z"
# }| Field | Description |
|---|---|
| status | pending · running · completed · rejected · failed |
| content | Generated text (null if not completed) |
| quality_score | 0–10 (null if rejected/failed) |
| voice_version | SHA-1 of the voice JSON at time of generation (audit trail) |
| verdict | APPROVE — passed review, delivered if score ≥ 6 · REQUEST_CHANGES — issues found, system auto-patches and delivers with advisory · REJECT — content blocked regardless of score |
| issues | List of flags raised by reviewer (if any) |
Status meanings: pending — in queue · running — generating · completed — ready · rejected — failed quality gate, does NOT count toward quota · failed — system error, does NOT count toward quota, retry.
Errors: 401 bad key · 404 job not found
Voice Profiles
POST /v1/voices
Create a custom voice profile from writing samples. Requires a paid plan (Starter or Pro).
curl -X POST https://content-apiapi-production.up.railway.app/v1/voices \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "My Newsletter Voice",
"samples": [
"Sample post 1: I started my career...",
"Sample post 2: Remote work changed everything...",
"Sample post 3: Here is why async matters..."
],
"hard_rules": ["no_emoji", "no_emdash", "contractions"],
"mode_default": "A"
}'
# Response 201
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "My Newsletter Voice",
# "voice_version": "abc123def456",
# "created_at": "2026-05-18T10:00:00.000Z"
# }| Field | Type | Required | Description |
|---|---|---|---|
| name | string | ✓ | Voice profile name (1–100 chars) |
| samples | string[] | ✓ | Writing samples (1–10 items; 3–5 recommended) |
| hard_rules | string[] | — | Style constraints: no_emoji, no_emdash, contractions, oxford_comma, etc. Default: ["no_emoji", "no_emdash"] |
| mode_default | string | — | Default voice mode: A · B · C — default A |
Errors: 400 invalid input · 401 bad key · 403 free plan can't create voices, or voice limit reached (Starter: 1, Pro: 5)
GET /v1/voices
List all custom voice profiles (metadata only). Raw voice JSON (samples + hard rules) is never returned. Free-plan users receive an empty voices array — this is 200 OK, not 403. Voice creation is blocked on POST, not GET.
curl https://content-apiapi-production.up.railway.app/v1/voices \
-H "x-api-key: capi_your_key"
# Response 200
# {
# "voices": [
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "My Newsletter Voice",
# "voice_version": "abc123def456",
# "created_at": "2026-05-18T10:00:00.000Z"
# }
# ]
# }Errors: 401 bad key
GET /v1/voices/:id
Get metadata for a single voice. Same shape as the list entries — no raw voice JSON exposed.
curl https://content-apiapi-production.up.railway.app/v1/voices/550e8400-e29b-41d4-a716-446655440000 \
-H "x-api-key: capi_your_key"
# Response 200
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "My Newsletter Voice",
# "voice_version": "abc123def456",
# "created_at": "2026-05-18T10:00:00.000Z"
# }Errors: 401 bad key · 404 voice not found
PUT /v1/voices/:id
Update a voice profile (name, samples, hard rules, etc.). Auto-bumps voice_version. Existing jobs keep the version active at their creation time.
curl -X PUT https://content-apiapi-production.up.railway.app/v1/voices/550e8400-e29b-41d4-a716-446655440000 \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "My Newsletter Voice — Updated",
"samples": ["Sample 1...", "Sample 2...", "Sample 3..."],
"hard_rules": ["no_emoji"]
}'
# Response 200
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "My Newsletter Voice — Updated",
# "voice_version": "new_sha1_version",
# "created_at": "2026-05-18T10:00:00.000Z"
# }Errors: 401 bad key · 404 voice not found
Auth Endpoints
POST /v1/auth/register
Create a new account.
curl -X POST https://content-apiapi-production.up.railway.app/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# Response 201
# {"message": "Verification email sent"}Errors: 400 invalid email · 409 email already registered
POST /v1/auth/login
Request a magic login link (sent to verified emails only).
curl -X POST https://content-apiapi-production.up.railway.app/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# Response 200
# {"message": "Login link sent to verified email"}Errors: 400 invalid email · 403 email not verified · 429 throttled (5min between sends)
GET /v1/auth/verify?token=TOKEN
Verify email and issue API key (called by clicking the email link).
curl https://content-apiapi-production.up.railway.app/v1/auth/verify?token=abc123... \
-H "Content-Type: application/json"
# Response 200
# {
# "key": "capi_xxx",
# "tier": "free",
# "message": "Save your key now — you won't see it again"
# }Errors: 400 invalid or expired token · 404 token not found
GET /v1/auth/me
Get current user info (plan, usage, email verified).
curl https://content-apiapi-production.up.railway.app/v1/auth/me \
-H "x-api-key: capi_your_key"
# Response 200
# {
# "email": "user@example.com",
# "tier": "starter",
# "email_verified": true,
# "subscription_valid": true,
# "subscription_expires_at": "2026-06-18T00:00:00Z"
# }Errors: 401 missing/invalid API key
GET /v1/auth/usage
Get monthly quota details.
curl https://content-apiapi-production.up.railway.app/v1/auth/usage \
-H "x-api-key: capi_your_key"
# Response 200
# {
# "tier": "starter",
# "monthly_limit": 100,
# "used_this_month": 23,
# "remaining": 77,
# "reset_at": "2026-06-01T00:00:00Z",
# "billing_cycle_started": "2026-05-18T00:00:00Z"
# }Errors: 401 missing/invalid API key
POST /v1/auth/resend-verification
Resend verification email.
curl -X POST https://content-apiapi-production.up.railway.app/v1/auth/resend-verification \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# Response 200
# {"message": "Verification email sent"}Errors: 400 invalid email · 403 email already verified · 429 throttled (wait 5min)
Webhooks
POST /v1/webhooks/test
Verify your webhook endpoint is reachable before relying on it.
curl -X POST https://content-apiapi-production.up.railway.app/v1/webhooks/test \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json" \
-d '{"webhook": "https://yoursite.com/webhooks/content"}'
# Response 200
# {"message": "Webhook is reachable"}Errors: 400 invalid URL · 503 endpoint unreachable
Webhook Delivery
When you set webhook on a generate request, the server POSTs to that URL when the job completes or is rejected:
POST <your webhook URL>
Content-Type: application/json
X-Content-Signature: sha256=<hex digest>
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"content": "Generated content text...",
"quality_score": 9,
"review_report": {
"verdict": "APPROVE",
"voice_score": 9,
"issues": [],
"patches": []
}
}Signature Verification (Node.js)
Each delivery includes X-Content-Signature: sha256=..., an HMAC-SHA256 of the raw request body. Verify in constant time before trusting the payload:
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(rawBody, signatureHeader, signingSecret) {
// signatureHeader = "sha256=abc123..."
const provided = signatureHeader.replace("sha256=", "");
const expected = createHmac("sha256", signingSecret)
.update(rawBody)
.digest("hex");
return timingSafeEqual(
Buffer.from(provided),
Buffer.from(expected)
);
}Retry policy: Max 3 attempts · backoff 1s, 2s, 4s · 10s timeout per attempt. Triggers on network error or non-2xx response. After all failures, content is still saved — poll GET /v1/content/:id.
Billing
GET /v1/billing/status
Get subscription status.
curl https://content-apiapi-production.up.railway.app/v1/billing/status \
-H "x-api-key: capi_your_key"
# Response 200
# {
# "tier": "starter",
# "subscription_active": true,
# "subscription_expires_at": "2026-06-18T00:00:00Z",
# "monthly_limit": 100,
# "used_this_month": 23,
# "remaining": 77
# }Errors: 401 missing/invalid API key
POST /v1/billing/checkout
Generate a payment link (PayOS).
curl -X POST https://content-apiapi-production.up.railway.app/v1/billing/checkout \
-H "x-api-key: capi_your_key" \
-H "Content-Type: application/json" \
-d '{"tier": "starter"}'
# Response 200
# {
# "checkout_url": "https://payment.payos.vn/...",
# "order_id": "order-uuid"
# }| Field | Type | Description |
|---|---|---|
| tier | string | starter · pro |
Errors: 401 bad key · 400 already on that plan
Rate Limits & Quotas
Monthly limits reset on the 1st of each month at 00:00 UTC.
| Plan | Monthly Limit | Content Types | Custom Voices |
|---|---|---|---|
| Free | 10 | caption, marketing | 0 |
| Starter | 100 | all | 1 |
| Pro | 500 | all | 5 |
| Enterprise | Custom | all | Unlimited |
Quota counting: Only pending, running, and completed jobs count. rejected and failed jobs do not consume quota. Check usage anytime via GET /v1/auth/usage.
Error Codes
| Code | Meaning | Example |
|---|---|---|
| 400 | Validation error | Invalid JSON, topic too short, invalid UUID |
| 401 | Missing or invalid API key | Missing x-api-key header |
| 403 | Forbidden — plan restriction | Free plan tried to create voice; free plan tried blog type |
| 404 | Resource not found | Job ID or voice ID doesn't exist |
| 409 | Conflict | Email already registered |
| 429 | Rate limited — quota exhausted | Monthly limit reached; resend throttled |
| 503 | Server error — queue unavailable | Temporary system issue, retry later |
Pipeline Overview
Tier Auto-Routing
If you omit tier, the system auto-routes based on type:
| Content type | Auto-routed tier |
|---|---|
| caption | basic |
| marketing, email | upgrade |
| blog, article, landing | pro |
Override only when you have a specific reason. Free plan users are always forced to basic regardless.
Quality Score Gate
A job reaches the caller only if both conditions are true:
- Verdict is APPROVE (no hallucinations, on-topic, passes review)
- quality_score ≥ 6
If score < 6, verdict becomes REJECT even if content looks acceptable. If verdict is REQUEST_CHANGES, the system attempts an auto-patch and delivers with an advisory. If verdict is REJECT, content is blocked.
Voice Version Snapshots
voice_version in a completed job is the SHA-1 hash of the voice JSON (name + samples + hard rules + mode_default) at the time the job ran. This creates an audit trail — old jobs keep their original version even if you later update the voice.
Quick Links
| Resource | Link |
|---|---|
| User Guide (non-technical) | /docs |
| Playground (browser) | /playground |
| Pricing | /pricing |
| Dashboard (account, usage, voices) | /dashboard |
| CLI (npm) | npm install -g @content-agent/cli |
Need help? Email support or check the User Guide FAQ.