# GhostShorts API > GhostShorts turns an article, a quiz topic, a story or a chat conversation into a narrated 1080x1920 (9:16) MP4 short. The API makes the same videos as the app, from your scripts, automation tools and AI agents, using your plan's videos and credits. ## 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 No key is needed for this file, the template guides, GET /v1/templates, GET /v1/options and GET /v1/openapi.json. ## Flow 1. Pick a template below and read its guide (inputs, defaults, a full example, credits, errors). 2. Optional and free: POST /v1/estimate with {"template", "inputs"} returns the cost. Some templates also have a free first step (POST /v1/scripts for article_video and quiz_video, POST /v1/previews for reddit_story). 3. POST /v1/videos with {"template": "", "inputs": {...}} and optional "webhookUrl". 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". 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 }. GET /v1/videos lists your API videos, newest first. GET /v1/me shows your plan, remaining allowance, limits and webhook signing secret. ## Templates - article_video (Article to 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. Guide: https://api.ghostshorts.com/v1/templates/article_video.txt - quiz_video (AI Quiz Video): A multiple-choice quiz short: question card, countdown, answer reveal with a fact line, narrated, over a looping background. Guide: https://api.ghostshorts.com/v1/templates/quiz_video.txt - reddit_story (Reddit Story): Your own Reddit-style story (title plus 800 to 5000 characters of text, written by you: nothing is generated for it), narrated by an AI voice with word-synced captions over gameplay footage, with a Reddit title card. Narration is mandatory: there is no silent option. Free first: POST /v1/previews returns a ~10 second narration teaser for 0 credits. Guide: https://api.ghostshorts.com/v1/templates/reddit_story.txt - fake_text_message (Text Messages): Your own chat conversation (written by you: nothing is generated for it) rendered as an iMessage, WhatsApp, Instagram or Tinder chat over gameplay footage, with each message voiced by an AI voice for "me" and one for "them". Returns one MP4; pass cutAfterMessage to get the exact millisecond where the next message appears, for splicing. Guide: https://api.ghostshorts.com/v1/templates/fake_text_message.txt ## Limits - 60 requests per minute per account, of which 10 may create videos. - Free tools (estimate, scripts, previews): 30 per hour and 100 per day. - 3 videos in progress at once per account (429 CONCURRENCY_LIMIT beyond that). - 20 new own-media URLs per day. - Public reference routes (this file, the guides, templates, options, OpenAPI): 60 per minute per IP address. Your current limits are on GET /v1/me under "limits". Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 carries Retry-After. ## Webhooks Pass "webhookUrl" (public https) on POST /v1/videos. When the video finishes we POST JSON { id, type, createdAt, data } where type is video.completed or video.failed and data is the same video object GET /v1/videos/{id} returns. Answer with any 2xx. Each delivery times out after 10 s and is retried 3 times (after 10 s, 60 s and 300 s); redirects are not followed. Signature: header X-GhostShorts-Signature: t=,v1=, where v1 is HMAC-SHA256 over "." keyed with webhooks.signingSecret from GET /v1/me with its whsec_ prefix removed. Reject a delivery whose t is more than 5 minutes old. In Node: const crypto = require('crypto'); function verifyGhostShorts(rawBody, header, signingSecret) { const p = Object.fromEntries(header.split(',').map((kv) => kv.split('='))); const mac = crypto.createHmac('sha256', signingSecret.replace(/^whsec_/, '')).update(p.t + '.' + rawBody).digest('hex'); return Math.abs(Date.now() / 1000 - Number(p.t)) < 300 && mac.length === (p.v1 || '').length && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(p.v1)); } ## Idempotency Send an "Idempotency-Key" header (any unique string up to 255 characters) on every POST, so a retry after a timeout never makes a second video. Keys are kept for 24 hours: - Same key, same body: the first answer is replayed with the header Idempotent-Replayed: true. For POST /v1/videos the replay shows the video as it is now (current status and downloadUrl). Nothing is created or charged twice. - Same key, different body: 422 IDEMPOTENCY_KEY_REUSED. - Same key while the first request is still running: 409 IDEMPOTENCY_IN_PROGRESS. ## 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). Reference with anchors: 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. - 401 UNSUPPORTED_TOKEN: The bearer token is not a GhostShorts API key (gs_live_...). - 401 UNAUTHENTICATED: The request reached an endpoint without an authenticated account. Send your API key. - 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 INSUFFICIENT_AI_CREDITS: Not enough AI credits for this step. - 402 VIDEO_ALLOWANCE_EXHAUSTED: This month's video allowance is used up. - 402 CLIPPING_ALLOWANCE_EXHAUSTED: This month's clipping minutes are used up. - 402 SOFT_CAP_REACHED: A fair-use limit for this period was reached. - 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_VOICE: That voice id is not available for this template. See GET /v1/options voices. - 400 UNKNOWN_TEMPLATE: No template with that id. See GET /v1/templates. - 400 TEMPLATE_NOT_AVAILABLE: The template has no such step through the API (for example POST /v1/scripts for reddit_story). The detail says what to call instead. - 400 NARRATION_REQUIRED: The video cannot be rendered without narration. - 400 INVALID_WEBHOOK_URL: webhookUrl must be a public https URL. - 400 SAFETY_REJECTED: The content was refused by our safety checks. Change the topic or text. - 400 INVALID_IDEMPOTENCY_KEY: The Idempotency-Key header is empty or longer than 255 characters. - 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. - 409 CANNOT_CANCEL: Only a queued video can be cancelled. - 410 LINK_EXPIRED: The download link is past its 7 days. GET /v1/videos/{id} again for a fresh one. - 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. - 500 INTERNAL_ERROR: Something failed on our side. Retry; the same Idempotency-Key is safe to reuse. - 503 RATE_LIMITER_UNAVAILABLE: Rate limiting is briefly unavailable, so the request was refused. Retry shortly. - 503 SERVICE_UNAVAILABLE: The video queue is briefly unavailable. Nothing was charged; retry shortly. A video that fails has "status": "failed" and "error": { code, message }. error.code is one of these (a few request codes above can also appear): - MEDIA_FETCH_FAILED: Your background or music URL could not be downloaded: not public https, not reachable, refused, or larger than 300 MB (video) / 30 MB (audio). - MEDIA_INVALID: The file at your URL is not a usable video or audio file. - MEDIA_TOO_LONG: The file at your URL is longer than 10 minutes. - MEDIA_QUOTA_EXCEEDED: More than 20 new media URLs today. Reuse a URL (free for 30 days) or wait. - BACKGROUND_UNAVAILABLE: The background video or category you picked has no usable footage. Pick one from GET /v1/options. - MUSIC_UNAVAILABLE: The music id is not in the library. Pick one from GET /v1/options music. - IMAGE_UNAVAILABLE: An avatar or photo id is not an image you can use. - SCRIPT_FAILED: The script could not be written. - PACK_FAILED: The quiz questions 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. - VOICE_TTS_FAILED: The voice provider failed while recording. - 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