API & MCP / Docs / Article to Video

API template: article_video

Article to Video API

Send an article URL or the text itself. GhostShorts writes a short script from it, narrates it with captions, and returns a 1080x1920 MP4 of about 30, 60 or 90 seconds.

What you send

  • source: a public URL we fetch, or the article text itself.
  • length: short (~30 s), standard (~60 s) or deep (~90 s).
  • tone: explainer, punchy or authority.
  • An optional narrator voice and caption style.

Every option, with its limits and defaults, is in the full guide below and at api.ghostshorts.com/v1/templates.

Make a video

1. Check it first. POST /v1/scripts writes the script for 0 credits and returns it, so you can read it before you make the video. POST /v1/estimate with the same body returns the cost without spending anything.

2. Create the video. One request; the answer is a video id, and the video is made in the background.

curl -X POST https://api.ghostshorts.com/v1/videos \
  -H "Authorization: Bearer $GS_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "template": "article_video",
  "inputs": {
    "source": { "kind": "url", "url": "https://example.com/my-post" },
    "length": "standard"
  }
}'

3. Get the MP4. Poll GET /v1/videos/{id} or pass webhookUrl. It usually takes about a minute; the downloadUrl works for 7 days.

From an AI assistant, connect the MCP server (https://api.ghostshorts.com/mcp) and ask for an article video: it calls create_video with template article_video for you.

Cost

Each video uses one video from your plan's monthly allowance, and AI narration is included on current plans within fair use. There is no separate API plan. See plans.

Full guide

Every input, a complete example request and response, credits, errors and timing, generated from the API itself. Paste it into any AI chat and it knows how to make this video.

Copies this guide (article_video.txt).
# GhostShorts API: Article to Video (article_video)

Turn an article, blog post or newsletter (URL or pasted text) into a narrated 9:16 short with word-synced captions over a looping background.
Output: one 1080x1920 (9:16) MP4.
This guide: https://api.ghostshorts.com/v1/templates/article_video.txt  All templates: https://api.ghostshorts.com/v1/llms.txt

## Base URL and auth
Base URL: https://api.ghostshorts.com
Auth: send your API key on every request as the header "Authorization: Bearer gs_live_..."
Create a key at https://ghostshorts.com/app/settings/api-keys (the key is shown once). Keys use your plan's videos and credits; there is no separate API plan. Plans: https://ghostshorts.com/pricing

## Flow
1. Optional and free: POST /v1/estimate with the same {"template", "inputs"} body returns the cost without spending anything.
2. Optional and free (0 credits): POST /v1/scripts with {"template": "article_video", "inputs": {...}} writes the script and returns it with a "projectId". Check it, then send that projectId on POST /v1/videos so it is reused instead of written again.
3. POST /v1/videos with {"template": "article_video", "inputs": {...}} (optional "webhookUrl", optional "projectId" from the free step). It answers 202 with the video object; keep its "id".
4. Poll GET /v1/videos/{id} every 10-20 s until "status" is "completed" or "failed" (or "cancelled" if you cancelled it). In between you may see queued, preparing_media, generating_script, narrating and rendering. A queued video can be cancelled with POST /v1/videos/{id}/cancel.
5. On "completed", "downloadUrl" is a public link (no key needed) valid for 7 days. Give it to the user exactly as returned. On "failed", "error" is { code, message }.
6. Iterating: to change a finished video, POST /v1/videos again with its "projectId". Renders 2 and 3 of a project are included (no video used); render 4 counts again. Leaving projectId out starts a new project and uses a new video. One video per project at a time. Send the full inputs again (the new version is applied).
Webhook (optional): pass "webhookUrl" (public https) and we POST { id, type, createdAt, data } to it, where type is video.completed or video.failed and data is the video object. Deliveries carry the header X-GhostShorts-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with webhooks.signingSecret from GET /v1/me minus its whsec_ prefix. Verification code: https://api.ghostshorts.com/v1/llms.txt
Idempotency: send an "Idempotency-Key" header (any unique string up to 255 characters) on every POST. Retrying with the same key and body within 24 hours replays the first answer, so nothing is created or charged twice. Only a success is kept: after a refusal (400, 402, ...) fix the request and resend it with the same key.
Several at once: POST /v1/videos/batch with {"items": [ {template, inputs, projectId?, webhookUrl?}, ... ]} (1 to 10). If the whole batch does not fit the allowance nothing is created. Video responses carry "account" { videosLeft, narrationsLeft, aiCreditsLeft, resetsAt }.

## Use this template from MCP
From an MCP client (https://api.ghostshorts.com/mcp, same API key): estimate_cost, generate_script, then create_video with template "article_video" and these inputs; poll get_video until completed. To change the video, call create_video again with its projectId (renders 2-3 of a project are free). create_videos queues up to 10 at once.

## Inputs (the "inputs" object for template "article_video")
Fields marked optional may be omitted; the default shown is applied. Unknown fields are refused with 400 INVALID_INPUT.
- source (object, required, one of 2 shapes below): Where the article comes from: a URL we fetch, or the text itself.
  when source.kind is url:
  - source.kind (string, required, always url)
  - source.url (string, required, URL): Public article, blog or newsletter URL.
  when source.kind is text:
  - source.kind (string, required, always text)
  - source.text (string, required, 1-60000 chars): Pasted article text. The script step needs 90 to 4000 words.
- length (string, optional, default standard, one of [short, standard, deep]): short ~30s, standard ~60s, deep ~90s.
- tone (string, optional, default explainer, one of [explainer, punchy, authority])
- voiceId (string, optional, default onwK4e9ZLuTAKqWW03F9, one of [onwK4e9ZLuTAKqWW03F9, JBFqnCBsd6RMkjVDRZzb, EXAVITQu4vr4xnSDxMaL, pNInz6obpgDQGcFmaJgB, LcfcDJNUP1GQjkzn1xUU, TxGEqnHWrfWFTfGW9XjX]): Narrator voice. See GET /v1/options voices tagged article_video.
- captionPresetId (string, optional, one of 17 ids listed with names at GET /v1/options): Caption style. See GET /v1/options captionPresets.
- silent (boolean, optional, default false): Render with no narration. Refused unless explicitly true.

## Example request
Set GS_KEY to your key first (export GS_KEY=gs_live_...).
curl -X POST https://api.ghostshorts.com/v1/videos \
  -H "Authorization: Bearer $GS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: article-video-example-001" \
  --data-binary @- <<'JSON'
{
  "template": "article_video",
  "inputs": {
    "source": {
      "kind": "url",
      "url": "https://paulgraham.com/google.html"
    },
    "length": "standard",
    "tone": "explainer"
  }
}
JSON

Response: 202 Accepted, header Location: /v1/videos/cmuha1rt0000108l4a7kq2x9d
{
  "id": "cmuha1rt0000108l4a7kq2x9d",
  "template": "article_video",
  "status": "queued",
  "stage": "Queued",
  "progress": null,
  "projectId": null,
  "exportId": null,
  "downloadUrl": null,
  "downloadExpiresAt": null,
  "error": null,
  "creditsCharged": null,
  "estimatedSecondsRemaining": 90,
  "createdAt": "2026-09-24T15:00:00.000Z",
  "completedAt": null,
  "cutAtMs": null,
  "notices": [
    "This script is an AI first draft of your article. Read it against your source before you publish; check any numbers, names and dates, and keep the claims yours.",
    "Works best on your own posts and any public page. Paywalled or login-only pages will not import; paste the text instead."
  ]
}

Once completed, GET /v1/videos/cmuha1rt0000108l4a7kq2x9d answers (example values):
{
  "id": "cmuha1rt0000108l4a7kq2x9d",
  "template": "article_video",
  "status": "completed",
  "stage": "Done",
  "progress": 100,
  "projectId": "cmuha1rt9000208l4f3bn6c1e",
  "exportId": "cmuha2b4x000308l4d9ye7h0s",
  "downloadUrl": "https://api.ghostshorts.com/v1/d/Zq3hR8uXk2mV7pLw4sNf9cJt1bYe6aGd0oKi5HrQyUE",
  "downloadExpiresAt": "2026-10-01T15:01:04.000Z",
  "error": null,
  "creditsCharged": {
    "narration": 0,
    "render": 0,
    "videos": 1
  },
  "estimatedSecondsRemaining": null,
  "createdAt": "2026-09-24T15:00:00.000Z",
  "completedAt": "2026-09-24T15:01:04.000Z",
  "cutAtMs": null,
  "notices": [
    "This script is an AI first draft of your article. Read it against your source before you publish; check any numbers, names and dates, and keep the claims yours.",
    "Works best on your own posts and any public page. Paywalled or login-only pages will not import; paste the text instead."
  ]
}

## Credits
- narration: 1 credit per minute of narration (legacy plans); included on current plans within fair use
- render: 1 video from the monthly allowance (current plans); export counter on legacy plans
Plan and balance are checked before anything runs, so a 402 refusal costs nothing. creditsCharged on the video says what was actually charged.

## Notes
- The script is condensed from the source and must stay faithful to it: every figure, quote and named person in the script must appear in the source.
- Source text must be 90 to 4000 words after extraction.
- Background footage is chosen server-side.

## Errors
Errors are JSON with Content-Type application/problem+json: { type, title, status, detail, code }. Branch on "code". 400 INVALID_INPUT adds "errors": [{ path, message, code }]. Every 402 adds "upsell" and "plansUrl" (https://ghostshorts.com/pricing). A 429 carries Retry-After. Full reference: https://ghostshorts.com/developers/errors
- 401 MISSING_API_KEY: No Authorization: Bearer header was sent.
- 401 INVALID_API_KEY: The key is unknown or has been revoked. Create a new one in Settings.
- 402 SUBSCRIPTION_REQUIRED: This step needs an active plan. The response carries plansUrl.
- 402 INSUFFICIENT_CREDITS: Not enough credits for this step. Top up or wait for your plan to renew.
- 402 VIDEO_ALLOWANCE_EXHAUSTED: This month's video allowance is used up.
- 402 SOFT_CAP_REACHED: A fair-use limit for this period was reached.
- 402 VOICE_FAIR_USE_REACHED: This month's voice allowance is used. Videos already started can still be finished; new narrations start again at the next renewal, or move up a plan.
- 400 VIDEO_TOO_LONG: Videos can be up to 10 minutes. Split this one into parts. Also the error.code of a video whose finished length came out over 10 minutes.
- 400 INVALID_INPUT: The body does not match the schema. The errors array names each field. Also sent as 413 for a body over 2 MB.
- 400 INVALID_WEBHOOK_URL: webhookUrl must be a public https URL.
- 404 NOT_FOUND: No such endpoint, or no such video, project or download link on this account.
- 409 IDEMPOTENCY_IN_PROGRESS: A request with this Idempotency-Key is still running. Retry after Retry-After.
- 422 IDEMPOTENCY_KEY_REUSED: This Idempotency-Key was already used with a different body. Use a new key for a new request.
- 429 RATE_LIMITED: Too many requests. Wait for Retry-After seconds.
- 429 CONCURRENCY_LIMIT: Three videos are already in progress on this account. Retry after one finishes.
- 503 SERVICE_UNAVAILABLE: The video queue is briefly unavailable. Nothing was charged; retry shortly.
- 400 NARRATION_REQUIRED: The video cannot be rendered without narration.
- 400 SAFETY_REJECTED: The content was refused by our safety checks. Change the topic or text.

A video that fails has "status": "failed" and "error": { code, message }; error.message always says what happened. Codes for this template:
- BACKGROUND_UNAVAILABLE: The background video or category you picked has no usable footage. Pick one from GET /v1/options.
- SCRIPT_FAILED: The script could not be written.
- INGEST_FAILED: The article at your URL could not be read. Paste the text instead.
- SOURCE_TOO_SHORT: The article is too short to make a script from.
- NARRATION_FAILED: The narration could not be recorded.
- RENDER_FAILED: The render failed. Create the video again.
- INTERRUPTED: Processing was interrupted. Create the video again; contact support if narration was charged.
- CANCELLED: You cancelled the video while it was queued.

## Timing
Renders usually take about a minute; own-media imports add about 30 seconds the first time; the first attempt occasionally takes several minutes before the automatic retry succeeds.

Machine-readable: https://api.ghostshorts.com/v1/openapi.json  Templates: https://api.ghostshorts.com/v1/templates  Options (voices, backgrounds, music, caption presets): https://api.ghostshorts.com/v1/options

More

Make an article video from code

Create a key, send one request, get an MP4 back in about a minute.

Create an API key