One API page, all sections
Use the tabs below to jump to the section you need. This page combines all API guides in one place so you can hand one link to a developer, workflow builder, or AI agent.
If you are not writing code, start with One-click create or AI assistant setup instead.
If you are planning a focused mobile or web product, also read Build a Story video app. If you are running many channels or clients, read Automate videos for multiple channels.
Who this page is for
Use this page if you are building an app, automation, or AI agent. If you just want to make videos yourself, start with the dashboard or AI assistant setup instead.
Base URLs
- Production:
https://trending.com
Tools (in the dashboard)
- /dashboard/developer — all developer tools in one place
- /dashboard/developer/api-keys — create/revoke API keys
- /dashboard/developer/triggers — signed triggers (no API key shared)
- /dashboard/developer/webhooks — webhook endpoints + delivery logs + exports
- /dashboard/developer/api-logs — API request logs (redacted request/response)
Building agent workflows? See AI assistant setup for API-key MCP setup and everyday usage flow.
Keep API keys private. Anyone who gets your key can send requests as your account and spend your credits. If a key might be exposed, revoke it immediately and issue a new key.
On /dashboard/developer/api-keys, each key now shows quick 24h request/error stats, and revoke uses a confirmation step to help prevent accidental lockouts.
Create a video (API)
You must send an Idempotency-Key for create requests. This prevents accidental duplicate videos and duplicate charges on retries.
POST /api/v1/story/videos
Authorization: Bearer YOUR_API_KEY
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
Content-Type: application/json
{
"preview_only": false,
"prompt": "A short scary story about a hotel keycard.",
"webhook": {
"url": "https://example.com/webhooks/trending",
"secret": "optional shared secret",
"events": ["trending.story.v001.*"]
}
}
Then either poll status or use webhooks:
- Poll:
GET /api/v1/story/videos/{video_id} - Story reuse sources:
GET /api/v1/story/character-sources(discover validreuse_characters_from_video_id+ character-sheet links) - Story reference images:
GET /api/v1/story/reference-images(discover validasset_idvalues by name) andPOST /api/v1/story/reference-images(upload jpg/png/webp, max 3MB, or send a direct HTTPSsource_url) - Story outro (optional):
GET /api/v1/story/videos/{video_id}/outro(inspect current outro config),PUT /api/v1/story/videos/{video_id}/outro(update line/speaker/reference/attachments),POST /api/v1/story/videos/{video_id}/outro/image(generate outro image) - Story slide animations (optional):
GET /api/v1/story/videos/{video_id}/slide-animationsto review per-slide costs/status,POST /api/v1/story/videos/{video_id}/slide-animationsto animate one selected slide (lip-sync to that slide’s narration audio is on by default),DELETE /api/v1/story/videos/{video_id}/slide-animations/{slide_index}to discard one generated clip and keep the image - Render later:
POST /api/v1/story/videos/{video_id}/render(sendIdempotency-Key) - Abort run:
POST /api/v1/story/videos/{video_id}/abort - Webhook: set
webhook.url
Note: For Story ad/outro workflows, recommended sequence is preview-first: configure outro, generate outro image, optionally animate selected slides, then render.
Note: Story slide animation is optional, can be expensive, and is per-slide only (no animate-all endpoint). Always show the per-slide credit estimate and require explicit user approval before animating each slide. Current pricing is a fixed 10 credits per derived second. Narration lip-sync is enabled by default for each slide when narration audio exists, and Studio falls back quietly if Kling cannot use the slide audio clip. Kling runs at most 3 Story slide animations platform-wide, so extra requests remain queued until a slot opens.
Readiness note: If you are unsure whether the blocker is plan access, template access, or credits, call POST /api/v1/account/readiness before create. The same logic powers the hosted MCP readiness check.
Poll responses include progress fields like step_label, status_detail, progress_percent,
eta_seconds, step_eta_seconds, pipeline_eta_seconds, Story timing details,
plus dashboard_url/preview_url and control_urls for lifecycle actions.
Triggers (incoming webhook style)
If you want a single endpoint that can start any video type, use triggers:
- API key trigger:
POST /api/v1/triggers/videos - Signed trigger (recommended for third-parties):
POST /api/v1/triggers/webhook
Signed triggers use X-Trending-Timestamp and X-Trending-Signature-V1 so you don’t need to share an API key.
We also support an optional X-Trending-Nonce header for replay protection.
Full guide: Incoming Triggers
Webhooks (outbound)
Webhooks are how we notify you about progress/completion without polling. By default we only deliver terminal events:
*.completed and *.failed.
To receive more events (queued/started/progress/render.*), set webhook.events as an allowlist (supports * wildcards).
Story slide animation lifecycle also supports optional events:
trending.story.v001.slide_animation.queued,
trending.story.v001.slide_animation.in_progress,
trending.story.v001.slide_animation.deleted,
trending.story.v001.slide_animation.completed,
trending.story.v001.slide_animation.failed.
Full guide: Webhooks
Logs & debugging
- Use /dashboard/developer/api-logs to inspect your most recent requests (redacted bodies).
- Use the key filter on /dashboard/developer/api-keys to quickly isolate requests from a single key.
- Use the 24h/7d window toggle on /dashboard/developer/api-keys to narrow logs by timeframe.
- Use /dashboard/developer/webhooks to view delivery attempts, response codes, and resend.
- Export webhook deliveries as CSV/JSON from the Webhooks page.
- Export your credit ledger at
/dashboard/billing/export/csv. - Use
/dashboard/billing/statementsfor date-ranged statement exports (CSV/PDF). - See Troubleshooting for common errors and how to debug.
Quick integration test
For a quick integration check, send one preview-only create request with webhook.url,
then verify both poll status and webhook delivery/signature validation.
Common errors
401 missing_api_key— sendAuthorization: Bearer422 idempotency_required— sendIdempotency-Key429 rate_limited— slow down; retry later402 insufficient_credits— add credits or reduce options403 membership_api_required— current plan does not include API access403 membership_mcp_required— MCP-identified request needs an MCP-enabled plan403 membership_template_required— selected template is not included in your current plan413 storage_quota_exceeded— upload exceeds remaining storage quota
What this is
The Trending.com API lets apps, automations, and AI agents create videos programmatically. Videos are generated in the background, so you start a video, then check status until it is ready.
If you are not coding, use the dashboard or MCP instead. This page is for builders.
Step 1 – Create an API key
- Log in to your account.
- Go to /dashboard/developer/api-keys.
- Click Create API key and copy the key (it is shown once).
- On that same page you can revoke keys and view recent request logs.
API key creation and API requests are membership-gated. If your plan does not include API access, requests return 403 membership_api_required.
Template access is also plan-gated. If your plan does not include the selected video type, create requests return 403 membership_template_required with allowed_templates and an upgrade_url.
Authentication
Send your API key as a Bearer token:
Authorization: Bearer YOUR_API_KEY
Normal API routes read API keys from headers only. JSON-body api_key support is limited to the Incoming Triggers endpoint below.
Using a webhook tool like Zapier/Make? You can also use our
Incoming Triggers
endpoint, which supports putting api_key in the JSON body — or using a signed trigger (no API key shared) created at /dashboard/developer/triggers.
Idempotency (recommended)
When you create a video, you must include an Idempotency-Key header. If you retry the same request,
we will return the existing run instead of creating a duplicate.
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
Credits pricing
Credits are charged per pipeline event. Current flat prices:
*.v001.script.generate= 3 credits*.v001.render= 30 credits*.v001.metadata.generate= 0 credits (included in render pricing)
Other event costs (for example slide images or narration units) are computed from the active pricing configuration.
Quote credits before create
Use this endpoint to estimate credits before calling create:
POST /api/v1/cost-estimate
Request body:
{
"template": "story",
"request": {
"preview_only": false,
"script": { "images_requested": 12 }
}
}
Response includes estimate.total_credits plus a breakdown:
{
"template": "story",
"template_version": "v001",
"estimate": {
"preview_only": false,
"slides": 12,
"narration_units": 27,
"script_credits": 3,
"image_credits": 120,
"narration_credits": 27,
"transcription_credits": 3,
"metadata_credits": 0,
"render_credits": 30,
"total_credits": 183,
"pricing_version": "2026-03-19.public-pricing-estimate-v002",
"pricing_source": "configured_pricing"
}
}
This is an estimate only. Final charges are enforced by the ledger at hold/capture time.
When preview_only=true, the estimate intentionally sets render_credits to 0. Full renders still use the fixed 30 credit render event.
Check readiness before create
Use this endpoint when you want a quick answer to “can this account create this request right now?” before you spend time on create retries or support debugging.
POST /api/v1/account/readiness
Request body:
{
"template": "rants",
"request": {
"preview_only": true,
"prompt": "A rant about fake friends who only text when they are bored."
}
}
Response includes account access plus template-specific blockers:
{
"account": {
"plan_code": "starter",
"plan_label": "Starter",
"api_access": false,
"mcp_access": false,
"available_credits": 50,
"allowed_templates": ["rants"]
},
"template_readiness": {
"template": "rants",
"template_access": true,
"required_credits": 54,
"available_credits": 50,
"credits_needed": 4,
"ready_to_create": false
}
}
Use this when you need a stable blocker answer like membership_api_required, membership_mcp_required, membership_template_required, or a credit shortfall before you call create.
How it works (simple)
- Start:
POSTto a create endpoint. You get avideo_idimmediately. - Wait: either poll with
GET, or provide a webhook URL. - Done: when completed, the response includes the final
render_url(if rendered). This is a short-lived signed download URL, not a permanent public CDN link.
When a render URL is present, responses also include retention metadata (retention + render.retention) so you can tell users when the link expires.
Treat render_url as a temporary file-delivery URL. Download the MP4 promptly if you need to keep it, and do not embed or hotlink it as if it were a stable CDN asset.
Auto title selection
Many video types generate social copy (3 title ideas + a description). We automatically pick the shortest generated title as the default social title.
If the video still has a draft placeholder name, we also promote that selected title to the video name. If you send your own name, your name is kept.
Endpoints
Each video type has:
POST /api/v1/{video_type}/videos— create/start pipelineGET /api/v1/{video_type}/videos/{video_id}— poll pipeline statusPOST /api/v1/{video_type}/videos/{video_id}/render— render later from a preview/draft (Idempotency-Keyrequired)POST /api/v1/{video_type}/videos/{video_id}/abort— cancel an active runPOST /api/v1/account/readiness— check plan/template/credit readiness before createGET /api/v1/library/folders— list your folder IDs plus candidate summaries for non-Story background selectionPOST /api/v1/library/folders/coverage— preflight folder sufficiency/auto-fill behavior before createGET /api/v1/voice-actors— list selectable narrated voices (name, gender, age, accent, preview_url, favorite flags)POST /api/v1/voice-actors/{voice_actor_id}/favorite— favorite/unfavorite a voice actor globally or per narrated templateGET /api/v1/story/character-sources— list reusable Story character-sheet sources for your accountGET /api/v1/story/reference-images— list saved Story reference images for your account (returnsasset_idvalues)GET /api/v1/story/outro-sources— list reusable Story outro sources for your account (returnskeyvalues you can reuse on create)POST /api/v1/story/reference-images— upload a Story reference image by multipart form-data or by direct HTTPSsource_urlGET /api/v1/story/videos/{video_id}/outro— read Story outro config (line/speaker/reference options)PUT /api/v1/story/videos/{video_id}/outro— update Story outro config (line/speaker/reference/attachments)POST /api/v1/story/videos/{video_id}/outro/image— generate Story outro image from current outro settingsGET /api/v1/story/videos/{video_id}/slide-animations— list optional Story per-slide animation options/status/costsPOST /api/v1/story/videos/{video_id}/slide-animations— animate one selected Story slide (timings-derived duration only, billed at10 credits/second; lip-sync to that slide’s narration audio is on by default; queues automatically if all 3 platform-wide Kling slots are busy)DELETE /api/v1/story/videos/{video_id}/slide-animations/{slide_index}— delete one Story slide animation and fall back to image
Template-specific create/poll endpoints:
- Story:
POST /api/v1/story/videos,GET /api/v1/story/videos/{video_id},GET /api/v1/story/character-sources,GET /api/v1/story/reference-images,GET /api/v1/story/outro-sources,POST /api/v1/story/reference-images,GET /api/v1/story/videos/{video_id}/outro,PUT /api/v1/story/videos/{video_id}/outro,POST /api/v1/story/videos/{video_id}/outro/image,GET /api/v1/story/videos/{video_id}/slide-animations,POST /api/v1/story/videos/{video_id}/slide-animations,DELETE /api/v1/story/videos/{video_id}/slide-animations/{slide_index} - DM Story:
POST /api/v1/dmstory/videos,GET /api/v1/dmstory/videos/{video_id} - Confession:
POST /api/v1/confession/videos,GET /api/v1/confession/videos/{video_id} - Did You Know:
POST /api/v1/didyouknow/videos,GET /api/v1/didyouknow/videos/{video_id} - Rants:
POST /api/v1/rants/videos,GET /api/v1/rants/videos/{video_id} - Ranking:
POST /api/v1/ranking/videos,GET /api/v1/ranking/videos/{video_id} - List:
POST /api/v1/list/videos,GET /api/v1/list/videos/{video_id},GET /api/v1/list/videos/{video_id}/script,PUT /api/v1/list/videos/{video_id}/scriptorPATCH /api/v1/list/videos/{video_id}/script,POST /api/v1/list/videos/{video_id}/script/ctas
Polling for status
Poll every 5 to 15 seconds. For render completion checks, expect several minutes under normal load.
GET /api/v1/{video_type}/videos/{video_id}
When completed, the response includes:
request_id(use this to find the request in/dashboard/developer/api-logs)status(example:queued,running,completed,failed)current_step(what step is happening now)step_label,status_detail,step_number,total_steps,progress_percenteta_seconds(legacy step ETA alias),step_eta_seconds,pipeline_eta_seconds, andagent_status_message(latest pipeline narration)queue_wait_seconds,active_processing_seconds,step_elapsed_seconds, and timestamps (queue_started_at,processing_started_at,processing_finished_at,step_started_at)timingfor measured/average seconds (Story currently includes per-step + per-slide timing stats)story_progress+long_running_noticefor Story slide-heavy runsassetsfor Story (character sheet URL, attached reference-image URLs, per-slide attachment plan, per-slide media URLs,assets.outroconfig/image state, optional slide-animation summary, and narration audio URL while the pipeline runs)dashboard_url+preview_url+requires_auth_for_preview(login required to open the wizard preview)control_urls.poll,control_urls.start_render, andcontrol_urls.abortfor lifecycle controlbackground_selection_summaryon non-Story templates (folder-first counts plus fallback policy metadata)render.render_url(short-lived signed download URL for the final video when rendered)retentionandrender.retention(keep_days,expires_at,notice,admin_exempt) for download-window UXwarnings[]on non-Story templates (auto-fill/underfill/upload-profile notices for folder-first selection)pipeline_error(only when failed; sanitized envelope withcode+ safemessage, plus optionalstep/category/retryable/hint)
If your product needs a long-lived copy, download the file when it is ready and store your own reference. Do not rely on render.render_url as a permanent asset URL.
Webhooks (optional)
If you provide webhook.url in your create request, we send an HTTP POST when the video finishes or fails.
Full guide: Webhooks tab
Webhook options:
webhook.url— where to send the webhookwebhook.secret— optional secret to sign the payloadwebhook.events— optional allowlist (supports*wildcards). If omitted, we only deliver terminal events (*.completedand*.failed).
Events look like:
trending.story.v001.completed,trending.story.v001.failed(default)trending.story.v001.queued,trending.story.v001.started(optional)trending.story.v001.progress(optional; step changes)trending.story.v001.slide_animation.completed,trending.story.v001.slide_animation.failed(terminal events; optional for Story slide animation lifecycle)trending.story.v001.slide_animation.queued,trending.story.v001.slide_animation.in_progress,trending.story.v001.slide_animation.deleted(optional; include inwebhook.eventsto receive non-terminal animation updates)trending.story.v001.outro_image.completed,trending.story.v001.outro_image.failed(terminal events; optional for Story outro-image lifecycle)trending.story.v001.outro_image.queued(optional; include inwebhook.eventsto receive non-terminal outro-image updates)trending.list.v001.script.generated,trending.list.v001.script.updated(optional non-terminal List Step-2 lifecycle events; include inwebhook.eventsallowlist)trending.story.v001.render.started,trending.story.v001.render.completed,trending.story.v001.render.failed(optional)- …and the same pattern for the other video types.
Example allowlists:
{
"webhook": {
"events": [
"trending.story.v001.*",
"trending.*.v001.completed",
"trending.*.v001.failed"
]
}
}
Preview-only (optional)
If you set preview_only to true, we stop before the final render. The status becomes completed, but render.render_url stays null until you render the video later in the website.
Limits & rate limiting
- Requests may be rate-limited (
429) if you send too many create/poll calls too quickly. - Very large request bodies may be rejected (
413). - Default API request-body cap is
3,145,728bytes (3MB). - For current limits, see /dashboard/developer.
Create requests (by video type)
All create endpoints return immediately with a video_id. Rendering happens in the background.
Voice note for narrated auto-cast templates (story, dmstory, confession,
didyouknow, rants, list): if you do not set explicit voice actor IDs (where supported),
Studio may auto-pick from your saved favorites. Order: template favorites first, then global favorites, then normal defaults.
Use GET /api/v1/voice-actors to show users voice choices by name, gender,
age, accent, and preview_url. Use
POST /api/v1/voice-actors/{voice_actor_id}/favorite to favorite/unfavorite globally or for a specific narrated template.
Music note for narrated templates (story, dmstory, confession,
didyouknow, rants, list): send a music object to control
background music. Use music.enabled to turn it on, optional music.music_id (or music.id)
for a specific track, optional music.category for mood, and optional music.volume_percent
(default 20). If no track ID is set, Studio tries favorite tracks that match mood first, then a random accessible track.
Background folder note for non-Story templates (dmstory, confession, didyouknow,
rants, ranking, list): you can send background.folder_id to prefer a Library folder.
Studio picks folder clips first, then auto-fills from the template fallback pool when needed.
- Folder ID must belong to your account, or create returns
422 invalid_background_folder_id. - Folder entries can include your uploads and shared
library:{id}items you added to that folder. - Fallback scope: narrated templates + List use approved shared ASMR clips; Ranking uses approved category-based clips.
- If folder underfill strategy is set to
error, runs fail withpipeline_error.code=background_folder_underfilledinstead of auto-filling. - Create + poll responses include
background_selection_summaryandwarnings[]so clients can inspect folder-first counts, fallback policy, and auto-fill notices.
Library folder helper endpoints
GET /api/v1/library/folders— returns caller-owned folders with counts for user/shared/music/image items, upload-profile counts, and candidate summaries for background/ranking scope.POST /api/v1/library/folders/coverage— acceptstemplate+ optionalfolder_id,required_count,required_total_duration_seconds, andranking_category_preset. Response includes eligibility counts/duration, prediction, andwarnings[].POST /api/v1/library/videos— upload one MP4/MOV/WebM/M4V clip (max 200MB), optionally add it to a caller-owned folder, and queue downloader processing. Optional fields:name,folder_id,upload_profile(backgroundorranking). Local uploads are media-probed before they are accepted and must include a readable video stream, be at least 500px wide, and be at least 10 seconds long.POST /api/v1/library/video-urls— import one direct HTTPS MP4/MOV/WebM/M4V file URL, optionally add it to a caller-owned folder, and queue downloader processing. Optional fields:name,folder_id,upload_profile. Social post URLs and generic webpages are not supported. URL imports are downloader-validated before the asset becomes available and use dedicated upload throttles in addition to normal API auth checks.GET /api/v1/library/uploads/{asset_id}— poll the latest status for one caller-owned Library upload/import asset. Returnsasset.status, readiness, final/preview URLs when available, batch counters/status when known, source metadata (source_type,source_host, original file name), media facts such as duration, resolution, size, audio presence, codecs, and bitrate, plus structured uploadwarnings[]and retained-source details when Studio kept a short-lived archive copy of a local upload. While the asset is still processing, the response also includespoll_after_seconds.
Coverage preflight returns 422 invalid_background_folder_id when folder_id is not owned by the caller.
For ranking, invalid preset keys return 422 invalid_ranking_category_preset. Library video upload/import returns 422 invalid_library_folder_id when folder_id is not owned by the caller and 413 storage_quota_exceeded when the upload exceeds remaining storage. Local multipart uploads can also return 422 invalid_library_video_upload when the uploaded file fails media-probe validation.
Use POST /api/v1/library/videos when the file lives on the caller machine and you need to send bytes directly to Studio. Use POST /api/v1/library/video-urls when you already have a direct HTTPS file URL. Hosted MCP local-file helpers use MCP-runtime file paths, so they are not a substitute for browser/API multipart upload of a caller-local file.
After either upload/import path, poll GET /api/v1/library/uploads/{asset_id} until asset.status becomes available or failed. A queued upload is not ready to use in a Library folder-backed create request until it reaches available.
Create a Story video
POST /api/v1/story/videos
Request body (all fields optional):
{
"name": "Optional video name",
"prompt": "Optional story idea (min 6 chars if provided)",
"preview_only": false,
"script": {
"images_requested": 12,
"voice_format": "dialogue",
"narrator_enabled": false,
"visual_style_preset": "semi_realistic_painterly",
"visual_style_custom": null,
"reuse_characters_from_video_id": null,
"reuse_story_mode": "different",
"reference_images": [
{ "asset_id": 501, "alias": "logo_ref", "description": "CPALead logo mark" },
{ "story_id": 456, "run_id": 9912, "alias": "mascot_sheet", "description": "Mascot style reference" }
],
"reference_instruction": "Use @logo_ref on the jersey chest only.",
"outro": {
"enabled": true,
"text": "This story was brought to you by SkyLaunch.",
"speaker": "Ava"
}
},
"music": {
"enabled": true,
"category": "Dramatic",
"volume_percent": 20
},
"webhook": {
"url": "https://example.com/webhooks/trending",
"secret": "optional shared secret",
"events": ["trending.story.v001.completed", "trending.story.v001.failed"]
}
}
Parameter mapping (Story)
name(string, max 180) — optional video name for your library.prompt(string, 6 to 1000 chars) — optional story idea. If omitted, we pick a prompt for you.preview_only(boolean) — if true, stops before the final render so you can render later.script.images_requested(integer) — how many slides to generate (12 by default). Allowed range is 10 to 30 for normal runs, or 2 to 30 whenpreview_only=true.script.voice_format(string enum) —dialogue,monologue, ornarrated.- If explicit voice IDs are not sent, Story may auto-use favorites using script gender/age when available. Story also avoids using the same voice for two different characters when an alternate match exists.
- Caption defaults are auto-applied during creation. Per-character presets are seeded by speaker order: first
24, second33, third36, fourth41(additional speakers fall back to the base Story preset). script.narrator_enabled(boolean) — only used whenvoice_format=dialogue.script.visual_style_preset(string enum) — one of the presets below, orcustom.script.visual_style_custom(string, max 240) — only used whenvisual_style_preset=custom.script.reuse_characters_from_video_id(integer) — optional. Use characters from one of your past Story videos. CallGET /api/v1/story/character-sourcesto discover valid IDs and character-sheet URLs.script.reuse_story_mode(string enum) —different(default),similar(standalone new episode, not continuation), orcontinuation. If you usesimilarorcontinuation, you must also setreuse_characters_from_video_id.script.reference_images(array, max 9) — optional Story references. Each entry must be either{asset_id, alias?, description?}(saved reference image) or{story_id, run_id, alias?, description?}(previous Story run image you own).descriptionis optional (max 220) and recommended when a filename is ambiguous.script.reference_instruction(string, max 600) — optional extra instructions for how to use the attached references.script.outro(optional object) — setenabled,text(max 900), andspeaker(max 120 orauto) to define the outro/ad line during script creation.script.outro.source_key(string, optional) — reuse a saved Story outro by passing akeyfromGET /api/v1/story/outro-sources.- Story script output plans where references are used:
character_sheet_reference_aliases+slide_prompts[].reference_aliases. Slide generation then attaches only aliases planned for that slide. @aliastokens are visual-only and should appear in visual prompts only; spoken fields remain human-readable (no handle/file-name speech).- Slide prompts for slides
2..Ninclude explicit prior-scene continuity guidance (what persists from the previous slide) unless intentionally changed. - Continuity behavior (default on): Story slide generation is ordered. If slide
N-1is missing or failed, slideNwaits instead of generating out of order.
Story reference image helper endpoints
GET /api/v1/story/reference-images— returns saved reference images for your account, includingasset_id,name,image_url, and analias_suggestion(sorted newest first).POST /api/v1/story/reference-images— upload one image (jpg/jpeg/png/webp, max 3MB) by multipart form-data, or send a direct HTTPSsource_url, and receive a newasset_id. Uploads also enforce remaining storage quota; if exceeded you receive413 storage_quota_exceeded. Uploading reference images does not consume credits.
Story outro helper endpoints
GET /api/v1/story/outro-sources— returns reusable saved outro sources for your account. Use the returnedkeyasscript.outro.source_keyon create.GET /api/v1/story/videos/{video_id}/outro— returns current outro settings, speaker options, reference slide options, selected outro image state, and image-generation estimate fields.PUT /api/v1/story/videos/{video_id}/outro— updates outro line/speaker + optional image prompt/reference slide/attachment asset IDs (max 4 attachments).POST /api/v1/story/videos/{video_id}/outro/image— generates the outro image from character sheet + selected reference slide + optional attachment assets, returningrun_id+ charged credits metadata.- Attachment IDs come from existing Story reference-image assets (same
asset_idvalues returned by Story reference-image helper endpoints). - Recommended flow: create with
preview_only=true, configure/generate outro media, optionally animate selected slides, then call render.
Visual style presets (Story)
semi_realistic_painterly(default)anime3d_animatedphotorealisticcel_shaded_cartooncomicwatercolor_storybookflat_vectorpencil_sketchoil_paintingneon_cyberpunknoirclaymationknitted_yarnlow_poly_3dcustom
What “custom” means (Story)
If you set script.visual_style_preset to custom, set script.visual_style_custom to your own style prompt.
{
"script": {
"visual_style_preset": "custom",
"visual_style_custom": "semi-realistic painterly illustration, warm rim light, soft shadows, cinematic depth of field"
}
}
Create a DM Story video
POST /api/v1/dmstory/videos
Main options:
prompt(min 6 chars if provided)script.topic_preset(optional) — one of:accidental_text,caught_in_a_lie,best_friend_drama,group_chat_exposed,boss_text,mysterious_number,receipt_drop,family_drama,roommate_drama,petty_argumentvoice.speed_percentage(80–200) andvoice.remove_silences(boolean)background.folder_id(integer, optional) — pick from your folder first, then fallback to approved shared ASMR clips.music(optional) —enabled, optionalmusic_id/id, optionalcategory, optionalvolume_percent(default 20)- If explicit voice IDs are not sent, DM Story may auto-use favorites by speaker gender and keeps the two speakers on different voices when possible.
preview_only(boolean)
Create a Confession video
POST /api/v1/confession/videos
Main options:
prompt(min 6 chars if provided)script.topic_preset(optional) — one of:embarrassing,school,friend_drama,relationship,family,work,money,petty,secret,mistakevoice.speed_percentage(80–200) andvoice.remove_silences(boolean)background.folder_id(integer, optional) — pick from your folder first, then fallback to approved shared ASMR clips.music(optional) —enabled, optionalmusic_id/id, optionalcategory, optionalvolume_percent(default 20)- If explicit voice IDs are not sent, Confession may auto-use favorites and match against
confessor_genderwhen available. preview_only(boolean)
Create a Did You Know video
POST /api/v1/didyouknow/videos
Main options:
prompt(min 6 chars if provided)script.topic_preset(optional) — one of:animals,food,space,history,science,everydaybackground.exit_enabled(boolean),background.exit_video_id(integer), and optionalbackground.folder_id(integer)voice.speed_percentage(80–200) andvoice.remove_silences(boolean)music(optional) —enabled, optionalmusic_id/id, optionalcategory, optionalvolume_percent(default 20)- If explicit voice IDs are not sent, Did You Know may auto-use favorites (no gender/age filter required).
preview_only(boolean)
Create a Rants video
POST /api/v1/rants/videos
Main options:
prompt(min 6 chars if provided)script.topic_preset(optional) — one of:wholesome,school,friend_drama,relationship,fake_friend,petty_revenge,rich_kid,glow_up,scammer,strict_parentsvoice.speed_percentage(80–200) andvoice.remove_silences(boolean)background.folder_id(integer, optional) — pick from your folder first, then fallback to approved shared ASMR clips.music(optional) —enabled, optionalmusic_id/id, optionalcategory, optionalvolume_percent(default 20)- If explicit voice IDs are not sent, Rants may auto-use favorites and match against
ranter_genderwhen available. preview_only(boolean)
Create a Ranking video
POST /api/v1/ranking/videos
Main options:
prompt(min 6 chars if provided)ranking.category_preset(optional) — one of:bodycam,security_camera,cinematic,animals,cartoon_characters,celebrities,vehicles,funny,cursed,absurd,wholesome,epic,chase,transformation,disaster,asmrbackground.folder_id(integer, optional) — pick from your folder first, then fallback to approved category-based clips.preview_only(boolean)
Create a List video
POST /api/v1/list/videos
Main options:
prompt(min 2 chars if provided) — if omitted, we pick a topic.script.item_count(1–15)script.include_ai_ctas(boolean, optional, defaulttrue) — setfalseto disable AI-generated CTA lines.- Generated list script lines may include bracketed emotion/sound cues for narration (for example
[laughs],[whispers],[crowd gasps]). Treat these as audio/performance cues, not visual actions, and avoid[PAUSE]tags. script.category(optional) — facts, tips, jokes, trivia, myths, and morescript.subcategory(optional) — only valid when a category is selectedvoice.voice_actor_id(integer, optional; fallback default is 233)background.folder_id(integer, optional) — pick from your folder first, then fallback to approved shared ASMR clips.music(optional) —enabled, optionalmusic_id/id, optionalcategory, optionalvolume_percent(default 20)- If
voice.voice_actor_idis omitted, List may auto-use favorites before default fallback voice selection. - Background clips: auto-selected from our ASMR clip library (hook + each item)
- Caption style defaults are auto-applied: preset
24(Hyperlegible), single-line captions with auto-shrink starting around18visible characters,font_scale=215%, glow effect, emoji size250withpop + shake,blurred_background=false,force_portrait_backgrounds=true. preview_only(boolean)
List script editing endpoints (Step 2 parity)
GET /api/v1/list/videos/{video_id}/script— fetch editable intro + ordered lines + CTA metadata +script.ctas[]+script.emoji_suggestions[]+script.updated_at.PUT /api/v1/list/videos/{video_id}/scriptorPATCH /api/v1/list/videos/{video_id}/script— save full ordered script edits (line move/delete/edit, intro edits, CTA line type/source, optionalcta_meta, optionalemoji_suggestions).POST /api/v1/list/videos/{video_id}/script/ctas— CTA helper endpoint withmode=suggest|apply, placement controls, and optional replace-existing behavior.- Optimistic lock support: send
expected_updated_atin write payloads (or sendIf-Matchwith priorETag). Stale writes return409 script_version_conflict. - Script responses include
ETagwhenscript.updated_atis available. - When narration is attached, updates require
reset_audio=true; otherwise API returns409 audio_locked. - While a pipeline run is
queuedorrunning, updates return409 pipeline_run_active.
What this is
This is a single “webhook-style” endpoint you can POST to in order to start a video. It is useful for automation tools (Zapier, Make, custom webhooks) that prefer one URL and simple JSON.
This is different from outbound webhooks (where we notify you when the video completes).
Endpoints
You have two options:
POST /api/v1/triggers/videos (API key)
POST /api/v1/triggers/webhook (signed trigger, no API key)
Option A – API key triggers
Use this when you control the system sending requests and can safely store an API key. (Create keys at /dashboard/developer/api-keys.)
Authentication supports 3 ways:
- Recommended:
Authorization: Bearer YOUR_API_KEY X-Trending-Api-Key: YOUR_API_KEYapi_keyin the JSON body (helpful for webhook tools; supported on this trigger endpoint only)
Option B – Signed triggers (no API key)
Use this when you don’t want to share an API key with a third-party tool.
Create a signed trigger at /dashboard/developer/triggers to get a trigger_id + secret.
Send these headers:
X-Trending-Trigger— yourtrigger_idX-Trending-Timestamp— Unix seconds (string). Requests must be within ~5 minutes.X-Trending-Signature-V1: sha256=...— HMAC of{timestamp}.{raw_body}using the trigger secretX-Trending-Nonce— optional but recommended unique nonce (replay protection)
Signature pseudo:
expected = "sha256=" + HMAC_SHA256(timestamp + "." + raw_body, trigger_secret)
Required fields
video_type— one of:story,dmstory,confession,didyouknow,rants,ranking,list
All other fields are the same as that video type’s normal create endpoint.
Idempotency (required)
Triggers require idempotency so retries don’t create duplicates.
If your tool can set headers, send Idempotency-Key.
If it cannot, you can send idempotency_key in the JSON body and we will treat it like the header.
{
"video_type": "story",
"idempotency_key": "123e4567-e89b-12d3-a456-426614174000"
}
Example (API key in JSON body)
POST /api/v1/triggers/videos
Content-Type: application/json
{
"api_key": "YOUR_API_KEY",
"video_type": "story",
"idempotency_key": "123e4567-e89b-12d3-a456-426614174000",
"preview_only": false,
"prompt": "A short scary story about a hotel keycard.",
"webhook": {
"url": "https://example.com/webhooks/trending",
"secret": "optional shared secret",
"events": ["trending.story.v001.completed", "trending.story.v001.failed"]
}
}
Example (signed trigger)
# Bash example (requires openssl)
TRIGGER_ID="YOUR_TRIGGER_ID"
SECRET="YOUR_TRIGGER_SECRET"
TS="$(date +%s)"
NONCE="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)"
BODY='{"video_type":"story","idempotency_key":"123e4567-e89b-12d3-a456-426614174000","preview_only":false,"prompt":"A short scary story about a hotel keycard."}'
SIG_HEX="$(printf "%s.%s" "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
curl -X POST "https://trending.com/api/v1/triggers/webhook" \
-H "Content-Type: application/json" \
-H "X-Trending-Trigger: $TRIGGER_ID" \
-H "X-Trending-Timestamp: $TS" \
-H "X-Trending-Signature-V1: sha256=$SIG_HEX" \
-H "X-Trending-Nonce: $NONCE" \
-d "$BODY"
Response
The response matches the normal create endpoint for that video type:
{
"video_id": 12345,
"pipeline_run_id": "6fba1c20-6d1c-4bb2-9a91-2c9f7fcb2d7c",
"status": "queued",
"poll_url": "/api/v1/story/videos/12345",
"webhook": { "enabled": true, "url": "https://example.com/...", "events": ["..."] }
}
Notes
- Use HTTPS in production and keep your API key / trigger secrets safe.
- If your webhook tool can add headers, prefer
Authorization: Bearer(standard). - Signed triggers are the safest option when you don’t want a third-party to store an API key.
- Optional: restrict a trigger by IP CIDRs in /dashboard/developer/triggers.
- To receive completion notifications, configure
webhook.urland follow the Webhooks tab.
What webhooks do
If you provide webhook.url in your create request, we send an HTTP POST when your video finishes
(*.completed) or fails (*.failed). Webhooks are designed for automation: update your app, notify users,
or start downstream processing without polling.
You can manage endpoints, rotate secrets, and view delivery logs at /dashboard/developer/webhooks.
Quickstart
- Create a webhook endpoint in your app that accepts JSON.
- Return a 2xx response quickly (do heavy work asynchronously).
- (Recommended) Add a
webhook.secretand verify the signature. - De-dupe deliveries using the
X-Trending-Deliveryheader (at-least-once delivery).
Webhook URL rules: use a public https:// endpoint on standard port 443 in production.
We reject loopback, private-network, metadata, .local, and .internal hosts. For local/dev smoke runs,
HTTP callbacks back to the configured Studio dev host are still allowed.
Headers
Every webhook request includes:
Content-Type: application/jsonUser-Agent: Trending-Webhooks/1.0X-Trending-Delivery— unique delivery id (use this to de-dupe)X-Trending-Event— the event name
If you set webhook.secret, we also include:
X-Trending-Timestamp— Unix seconds (string)X-Trending-Signature-V1: sha256=...— HMAC of{timestamp}.{raw_body}X-Trending-Signature: sha256=...— legacy (body-only)
Signature verification (recommended)
We sign each request using your webhook.secret. Always compute the signature on the exact raw bytes you received
(do not re-encode JSON).
// PHP (Laravel) example
$raw = $request->getContent();
$timestamp = (string) $request->header('X-Trending-Timestamp', '');
$secret = 'replace-with-your-webhook-secret';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
$received = (string) $request->header('X-Trending-Signature-V1', '');
if (!hash_equals($expected, $received)) {
abort(401);
}
To reduce replay risk, reject old timestamps (example: older than 5 minutes):
// Optional replay window check
$ts = (int) $timestamp;
if ($ts <= 0 || abs(time() - $ts) > 300) {
abort(401);
}
If you rotate secrets, accept both old and new secrets for a short overlap window to avoid dropped deliveries.
Event names
By default we deliver terminal events only:
trending.{video_type}.v001.completed
trending.{video_type}.v001.failed
You can opt into additional events by setting webhook.events in the create request (or by setting an event allowlist
on the saved endpoint in Webhook Tools). Allowlists support * wildcards.
trending.{video_type}.v001.queued
trending.{video_type}.v001.started
trending.{video_type}.v001.progress
trending.{video_type}.v001.render.started
trending.{video_type}.v001.render.completed
trending.{video_type}.v001.render.failed
Allowlist examples:
trending.story.v001.*
trending.*.v001.completed
trending.*.v001.render.*
Supported video_type values (v001):
story,dmstory,confession,didyouknowrants,ranking,list
Payload schema
Webhook payloads are JSON with a stable, compact shape:
{
"event": "trending.story.v001.completed",
"pipeline_run_id": "6fba1c20-6d1c-4bb2-9a91-2c9f7fcb2d7c",
"video_id": 12345,
"template": "story",
"template_version": "v001",
"status": "completed",
"preview_only": false,
"render_url": "https://trending.com/studio/renders/123/download?signature=...",
"occurred_at": "2026-01-27T03:12:45Z",
"error": null
}
Some events include extra fields:
*.progress—from_step,to_step*.render.*—render_job_id,render_job_status
On failures, error is present and render_url may be null. Successful payloads use a short-lived signed download URL.
Download the file promptly if you need to keep or distribute it. Do not treat webhook render_url as a permanent public asset or hotlink target.
{
"event": "trending.list.v001.failed",
"pipeline_run_id": "6fba1c20-6d1c-4bb2-9a91-2c9f7fcb2d7c",
"video_id": 12345,
"template": "list",
"template_version": "v001",
"status": "failed",
"preview_only": true,
"render_url": null,
"occurred_at": "2026-01-27T03:12:45Z",
"error": {
"code": "render_failed",
"message": "Render failed due to ...",
"step": "render"
}
}
Delivery semantics (important)
- At-least-once delivery: you may receive duplicates (retry). De-dupe by
X-Trending-Delivery. - Fast acknowledgement: respond 2xx quickly; enqueue processing on your side.
- Don’t trust IP allowlists: use signatures instead (safer across infra changes).
Retries
If your endpoint times out or returns a non-2xx response, we retry with exponential-ish backoff (up to 10 attempts):
10s, 30s, 2m, 10m, 30m, 1h, 2h, 4h (then continues until max attempts)
To stop retries, return a 2xx.
You can optionally override max attempts per endpoint in Webhook Tools (advanced).
Local testing
Two easy ways to test:
- Use Webhook Tools: visit /dashboard/developer/webhooks to send a signed test webhook.
- Expose localhost: use a tunnel like ngrok and set
webhook.urlto the public HTTPS URL.
# Example (ngrok)
ngrok http 3000
# Set webhook.url to the https URL ngrok prints:
# https://xxxx.ngrok-free.app/webhooks/trending
Integration smoke testing
For end-to-end verification, run one preview-only create request with a real webhook.url,
then confirm your receiver gets a terminal event and signature headers validate.
Delivery logs
You can view recent deliveries and resend them from /dashboard/developer/webhooks.
If a delivery is stuck in retrying and next_attempt_at is already in the past, resend from Webhook Tools or contact support.
Webhook Tools also supports exporting delivery logs (CSV and JSON) for auditing and debugging.
You can filter by video_id and pipeline_run_uuid in the UI.
Never share secret keys
Trending.com will never include internal service keys in API responses or webhook payloads. If you ever see sensitive data, treat it as a bug and rotate secrets immediately.
Outbound webhooks: verify signatures
If you set webhook.secret, we include:
X-Trending-Timestamp and X-Trending-Signature-V1.
Always compute HMAC on the raw request body bytes you received.
Recommended: reject timestamps older than ~5 minutes to reduce replay risk.
Guide: Webhooks
Signed triggers: avoid sharing API keys
If you’re integrating with a third-party automation tool (Zapier/Make/custom webhook sender), use signed triggers so you don’t need to store an API key in that system.
- Create triggers at /dashboard/developer/triggers
- Send requests to
POST /api/v1/triggers/webhook - Authenticate with
X-Trending-Signature-V1
Replay protection (recommended): X-Trending-Nonce
Signed triggers support an optional X-Trending-Nonce header.
When you send a nonce, we reject duplicates (replays) for that trigger.
Guide: Incoming Triggers
Optional IP allowlists
You can optionally restrict signed triggers to IP CIDRs in the Triggers dashboard. This is extra protection in addition to signatures.
Dashboard password reset abuse protection
Studio password reset requests are protected with layered throttling and privacy-safe responses. We return the same confirmation message whether an account exists or not, and apply rate limits plus short cooldown windows to reduce automated abuse.
Dashboard email verification
Studio account registration sends a verification email and requires verified email for authenticated dashboard access. This helps reduce fake-signup abuse and keeps lifecycle sends tied to real inbox ownership.
SES bounce and complaint suppression
Studio now ingests SES SNS feedback events for bounces and complaints and adds those addresses to a local suppression list. Future emails are skipped for suppressed recipients to protect sender reputation.
Email preference categories
Lifecycle emails (welcome and retention) honor per-category unsubscribe links. Render-complete product emails can also be toggled from account settings. Security/account-access emails remain separate transactional flows.
Authenticated Studio users can manage these preferences directly from /dashboard/settings.
Secret rotation
- Rotate webhook endpoint secrets at /dashboard/developer/webhooks.
- Rotate trigger secrets at /dashboard/developer/triggers.
- If rotating, accept both old and new secrets for a short overlap window on your side.
Where to look first
- /dashboard/developer — jump to all tools
- /dashboard/developer/api-logs — API request logs (redacted request/response)
- /dashboard/developer/webhooks — deliveries, attempts, response codes, resends, exports
- /dashboard/billing — user billing ledger (credits charged/refunded, video attribution, CSV export)
- /dashboard/billing/statements — statement period controls (weekly/monthly/custom) with CSV/PDF statement exports
- /dashboard/developer/triggers — trigger secrets, signed URL + headers
Use request_id to correlate everything
Most API responses include a request_id. Copy it into /dashboard/developer/api-logs
to find the exact request (including a redacted request/response body preview).
Common API errors
Errors are returned as:
{
"request_id": "…",
"error": {
"code": "…",
"message": "…",
"details": { }
}
}
401 – Authentication
missing_api_key— sendAuthorization: Bearer YOUR_API_KEY(orX-Trending-Api-Key/X-Api-Key). JSON-bodyapi_keyis only accepted onPOST /api/v1/triggers/videos.invalid_api_key— key is wrong/revoked; rotate at /dashboard/developer/api-keys.invalid_signature— for signed triggers, recompute HMAC on the raw body bytes exactly as received.nonce_reused— you reusedX-Trending-Nonce; generate a new nonce per request.
422 – Validation / missing required headers
idempotency_required— sendIdempotency-Keyfor create/trigger requests.validation_failed— checkerror.detailsfor field-level messages.
402 – Credits
insufficient_credits— add credits or lower requested options.- Subscription credits roll over with per-grant expiry windows (monthly grants: +2 months; annual grants: term end +2 months).
- Upgrades are applied immediately; downgrades are applied on the next successful renewal payment.
- Cancelled subscriptions remain active until term end, and payment-failed subscriptions do not retroactively claw back already-granted credits; failed renewals simply stop new cycle grants until payment succeeds.
- Use
/dashboard/videos/{video_id}/chargesto see a simple credit breakdown for one video, including credits set aside, used, or given back. - Export your full ledger at
/dashboard/billing/export/csv. - Use
/dashboard/billing/statementsto choose period ranges, then export at/dashboard/billing/statement/export/csvor/dashboard/billing/statement/export/pdf. - Promo and partner grants, including CPAlead credits after account verification, appear in the same billing ledger once they are posted.
- Internal expiry adjustments are tracked for accounting but hidden from user billing and statement exports.
403 – Membership gates
membership_api_required— current plan does not include API access.membership_mcp_required— MCP-identified requests require an MCP-enabled plan.membership_template_required— selected template is not included in the current plan; checkerror.details.allowed_templates.
429 – Rate limits
rate_limited— slow down and retry later. Limits apply per API key / per trigger (not IP-only).
413 – Request too large
request_too_large— reduce payload size (or send fewer nested settings at once).- Default API request-body cap is
3,145,728bytes (3MB). - Story reference-image uploads also enforce a per-file cap of 3MB.
- Multipart uploads include a small protocol-overhead allowance, so an actual 3MB image file can still pass.
503 – Temporary service issue
service_unavailable— temporary outage or maintenance. Wait a minute and retry.- When final renders are paused for maintenance, preview-only work can still remain available. Check the response details for
scope=final_render_onlyandpreview_only_available=true. - If it continues, contact support and include your
request_id.
Temporary generation-service load (Story image steps)
- If Story image generation is under heavy load, polling can fail with
pipeline_error.codesuch asslide_generation_failedorcharacter_sheet_generation_failed. - Messages are intentionally sanitized for safety across all templates. Raw internal diagnostic payloads are not returned in API, webhook, or MCP responses.
- For Story one-click, each slide now has a fail-fast cap of 2 total attempts (initial + one retry) before the pipeline stops.
Webhook deliveries: retries & timeouts
Webhooks can fail for many reasons: your server returned a 500, TLS/DNS errors, or slow response times. Trending.com retries deliveries automatically and records each attempt.
- View attempts at /dashboard/developer/webhooks
- Keep your receiver fast: respond
2xxquickly, then process async. - If you set
webhook.secret, always verify signatures before trusting the body.
If a delivery stays in retrying but next_attempt_at is already in the past,
trigger a resend from Webhook Tools or contact support.
If your receiver expects signatures but deliveries show as unsigned, set a webhook.secret on the request or endpoint.
Signed triggers: common pitfalls
- Clock drift: ensure your sender’s clock is accurate (timestamps outside the allowed window are rejected).
- Wrong body bytes: HMAC must be computed on the exact raw JSON sent (including whitespace).
- IP allowlists: if you configured CIDRs, requests from other IPs will be rejected.