This API, organized around REST principles, allows clients to upload audio or video files through a media creation endpoint, along with their transcription and translation job specifications, or to import subtitles they already have (SRT) for translation without uploading any media file. Subtitles corrected outside Fluen can be re-imported into an existing caption track (see Import Track Content), replacing its content and optionally restarting the translations in one call. Following processing, the transcribed and translated subtitle tracks can be retrieved, exported in all common subtitle formats, or burned into the video to produce an MP4 with hardcoded subtitles (a 'burn-in', which the web app calls an MP4 export). Webhooks report every job the moment it finishes, so no polling is needed.
The API uses API key authentication. To obtain an API key:
Include your API key in all requests using the X-API-Key header. The API key gives you access to operate on all your workspaces and their associated assets.
Important: Keep your API key secure and never share it publicly. If you suspect your API key has been compromised, you can revoke it and generate a new one from the same page.
By default, captions created through the API complete on their own and any translations
requested with them (or added later) start immediately: expect caption.completed
followed by translation.completed with no action needed in between.
Teams that want captions reviewed by an editor before translation can pass
captionPostEdit: true when creating the media. The caption track then waits in
POST_EDIT status after transcription, and translations stay SCHEDULED until the
captions are approved in the web editor. Approval is a web action; plan for it in
your workflow before enabling the flag.
Already have subtitles? The "Create Media from captions" operation builds a captions-only media from an inline SRT: no upload and no transcription runs, and the caption track is ready immediately. Add translation languages to it and export the results. Translations are billed on the SRT's duration; the imported captions are free.
Every Fluen job (encode, transcription, translation, burn-in) runs asynchronously and takes
anywhere from a few seconds to a few minutes to finish. Instead of polling
GET /api/media/{mediaId}, register a webhook endpoint
and Fluen will POST a signed JSON event to your URL the moment a job reaches a terminal state.
Manage endpoints with the /api/webhook-endpoints operations: register a URL plus an event-type
filter, list, update, delete, rotate the signing secret, send a test ping, and inspect recent
deliveries. The signing secret is shown only once, at creation and on rotate-secret. Each user
can have at most 50 active endpoints.
Payloads are outbound and so do not appear as operations in this reference. The event catalog, payload shape, and signature scheme below are the stable contract: field names are additive and are never renamed once shipped.
| Event type | Fired when | Key data fields |
|---|---|---|
media.ready |
media finished encoding | mediaId, mediaTitle, workspaceId, status, durationMs, mediaType |
media.failed |
encode or source-URL import failed | the above, plus errorMessage |
caption.completed |
a transcription track completed | mediaId, mediaTitle, workspaceId, trackId, languageId, jobType, status |
caption.failed |
a transcription failed | the above, plus reason (e.g. INSUFFICIENT_CREDITS) |
translation.completed |
a translation track completed (one event per language) | mediaId, mediaTitle, workspaceId, trackId, languageId, jobType, status |
translation.failed |
a translation track failed | the above, plus reason |
burnin.completed |
a subtitle burn-in finished | burnInId, mediaId, mediaTitle, workspaceId, trackId, videoFormat, status |
burnin.failed |
a subtitle burn-in failed | the above, plus errorMessage |
A synthetic ping event is sent only by the test-endpoint operation and is never matched by a
subscription.
Every delivery is an HTTP POST with Content-Type: application/json and this envelope:
{
"id": "evt_9f2c4e1abd7e4f0e8a1c6b5d4e3f2a10",
"type": "translation.completed",
"apiVersion": "2026-07-01",
"createdAt": "2026-07-10T14:12:09.482913Z",
"data": {
"mediaId": "aB3xK9",
"mediaTitle": "Q3 townhall.mp4",
"workspaceId": "ws_7Yn2",
"trackId": "trk_Fr8",
"languageId": "fr",
"jobType": "TRANSLATION",
"status": "COMPLETE"
},
"links": {
"media": "https://api.fluen.ai/api/media/aB3xK9",
"track": "https://api.fluen.ai/api/tracks/trk_Fr8"
}
}
id is stable across retries of the same event: use it to deduplicate. links are absolute
URLs to the canonical resources (GET), present when the event has a media/track. What you do
with a track (export it or burn it in) is a separate call (see below), not a link. Each
delivery also carries these headers:
| Header | Value |
|---|---|
X-Fluen-Event |
the event type, e.g. translation.completed |
X-Fluen-Delivery |
id of this delivery attempt-series |
X-Fluen-Signature |
t=<unix-seconds>,v1=<hex hmac-sha256>; see below |
The mediaId / trackId in data are the durable handles. Common follow-ups:
GET /api/tracks/{trackId}/export/{format}, where
{format} is one of SRT, VTT, TXT, DOCX, PDF, SSA, TTML, ITT, SBV, STL
(uppercase: the value binds case-sensitively).POST /api/tracks/{trackId}/burnin/{videoFormat} with no body (or {}) to render the track's
saved style, {videoFormat} one of ORIGINAL, HIGHEST, HIGH, STANDARD (see the operation
for what each resolution means). Wait for the burnin.completed event (or poll
GET /api/tracks/{trackId}/burnin), then fetch a time-limited download URL from
GET /api/tracks/{trackId}/burnin/{burnInId}/download.Each delivery is signed with your endpoint's secret so you can confirm it came from Fluen and was
not altered in transit. The X-Fluen-Signature header is t=<timestamp>,v1=<signature>, where the
signature is HMAC-SHA256(secret, "<timestamp>.<raw-body>") rendered as lowercase hex.
To verify: recompute the HMAC over timestamp + "." + rawBody with your secret and compare it to
v1 in constant time. Also reject deliveries whose timestamp is far from your clock: the
timestamp is bound into the signature to prevent replay.
const crypto = require('crypto');
function verifyFluenWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(',').map(kv => kv.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody)
.digest('hex');
const signatureOk = parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5-minute tolerance
return signatureOk && fresh;
}
Verify against the exact raw request body, before any JSON parsing or re-serialization.
id.2xx within a 10-second timeout. Anything else, including timeouts and
connection errors, is a failed attempt.410, the subscription is deactivated immediately with
no retries (REST Hooks convention). Return 410 when the receiving hook no longer exists.https URLs are accepted, and any URL resolving to a private or reserved IP range is
rejected at registration and again at delivery time.Endpoints follow the REST Hooks pattern, so Zapier instant triggers work directly (Zapier
subscribes by creating an endpoint and unsubscribes by deleting it). The
GET /api/webhook-endpoints/sample-events?type=<event-type> operation returns recent real events,
or a representative example, so Zap builders can map fields before a live event fires. Make.com
"custom webhook" triggers consume the same payloads.
Every plan, including the free one, can create API keys and use the full API surface
and webhooks. Free accounts are limited in the number of successful jobs they can run,
and their translations may be only partial. When a plan limit is reached, the API
responds 402 (or 403 for unavailable features) with code FREE_TIER_LIMIT and a
message that states the exact limit and carries an upgrade link; asynchronous jobs
report plan-limit failures through the failure webhooks' reason field (e.g.
FREE_DURATION_LIMIT). A partial translation carries isPartial: true and can be
completed after upgrading. Building or testing an integration and need more room than
the free plan allows? Email support@fluen.ai and tell us what you are working on.
Error responses carry a JSON body with a machine-readable code, a human-readable message,
and, where useful, structured details:
{
"code": "FREE_TIER_LIMIT",
"message": "Upgrade to translate into more languages.",
"details": { }
}
Codes in use: VALIDATION_ERROR (400), UNAUTHORIZED (401, missing or invalid credentials),
FREE_TIER_LIMIT / INSUFFICIENT_CREDITS (402, upgrade or top up to proceed), NOT_FOUND (404),
RATE_LIMITED (429), INTERNAL_ERROR (500). Asynchronous job failures surface as webhook
reason values instead: INSUFFICIENT_CREDITS, FREE_DURATION_LIMIT. New codes may be added
over time; existing ones are never renamed. A few legacy operations still return a bare message
string; switch on the HTTP status first and treat code as a refinement.
API-key requests are rate limited per account: 120 requests/minute overall, and
10 requests/minute for Create Media. Past the limit, requests get 429 with a
Retry-After header (seconds), X-RateLimit-Limit, and X-RateLimit-Remaining. Space out
polling loops (webhooks avoid most polling) and retry after the indicated delay.
Creates a new media entry using a publicly accessible URL to the source media file. You can also specify the languages and corresponding tasks (such as captioning or translation). The media URL must be accessible without authentication; for example, a public AWS S3 URL with a presigned token is supported. Alternatively, omit 'sourceUrl' to create the media in UPLOADING state, then upload the file directly via the 'Get raw upload URL' operation.
createMediaDto
| captionPostEdit | boolean Whether generated captions must be reviewed and approved in the web editor before translations start. Defaults to false for API requests: captions complete automatically and any requested translations start right away. Set true to keep the review workflow; the caption track then waits in POST_EDIT status and translations start once it is approved in the web editor. |
| fileName required | string The file name. The media title will be derived from this file name, excluding its extension. |
| importProvider | string Cloud picker provider for this import (e.g. GOOGLE_DRIVE). Requires providerFileId and providerAccessToken. |
| providerAccessToken | string Short-lived provider access token for cloud picker imports. Used once to stream the file; never stored. |
| providerFileId | string Provider-side file ID for cloud picker imports |
required | Array of objects (LanguageSelectionDto) The list of languages and job types associated with them |
| sourceUrl | string A public URL to the source media file |
| workspaceId required | string The ID of the workspace where this media gets uploaded into |
{- "captionPostEdit": true,
- "fileName": "string",
- "importProvider": "string",
- "providerAccessToken": "string",
- "providerFileId": "string",
- "selectedLanguages": [
- {
- "languageId": "string",
- "type": "CAPTION"
}
], - "sourceUrl": "string",
- "workspaceId": "string"
}Add a translation language to multiple media files at once. Responds 429 when your account has too many pending translation jobs (independent of the general rate limit); wait for jobs to finish and retry.
bulkAddTranslationDto
| mediaIds required | Array of strings List of Media IDs to add translation to |
required | object (LanguageSelectionDto) |
{- "mediaIds": [
- "string"
], - "translationLanguage": {
- "languageId": "string",
- "type": "CAPTION"
}
}Creates a captions-only media (no source file, no transcription) from an inline SRT of existing subtitles. Use it to translate subtitles you already have, from any source, without uploading the media file: the caption track is created COMPLETE and is immediately translatable via the add-translation endpoints. Duration is derived from the SRT timing and translations are billed on it; the imported captions themselves are free. The subtitle text must fit its timing at a realistic reading speed. Also used by the Premiere plugin to translate captions made in Premiere.
createMediaFromCaptionsDto
| diarization | string Enum: "DASH" "LABEL" "NONE" Speaker diarization style for the caption track (defaults to NONE) |
| maxCpl | integer <int32> Max characters per line for the caption track (optional) |
| maxLpc | integer <int32> Max lines per cue for the caption track (optional) |
| sourceLanguageId required | string The source language ID of the captions (e.g. 'en-US') |
| srtContent required | string The inline SRT content of the existing captions to import |
| title | string Optional media title; defaults to 'Imported captions' |
| workspaceId required | string The workspace to create the media in |
{- "diarization": "DASH",
- "maxCpl": 0,
- "maxLpc": 0,
- "sourceLanguageId": "string",
- "srtContent": "string",
- "title": "string",
- "workspaceId": "string"
}Retrieves a paginated list of media for a specific workspace with optional search. Results are sorted by creation date (newest first).
| page | integer <int32> Default: 0 Page number (0-indexed) |
| search | string Search term for filtering by title |
| size | integer <int32> Default: 50 Page size |
| wid required | string The Workspace Id |
Returns a lightweight list of all of a workspace's media (id, title, caption-track id, caption language, live translation count, and eligibility flags) for matching corrected SRT filenames to the media they belong to. Unlike the paginated read, this returns the whole workspace in one call with a small per-item payload (no full track lists). Responds 400 if wid is missing and 403 if the workspace is not readable by your account.
| wid required | string The Workspace Id |
Returns a presigned S3 PUT URL for uploading the raw source file of a media that was created with no sourceUrl (so it is still UPLOADING). The client must PUT the file to the returned 'uploadUrl' with the same 'contentType' as the Content-Type header. Once the file lands, encoding starts automatically. Used by the Premiere plugin's caption-from-audio flow.
createUploadUrlDto
| contentType required | string The Content-Type of the file being uploaded, e.g. 'audio/mp4' or 'video/mp4'. The client MUST send this exact value as the Content-Type header on the PUT; it is baked into the signature and determines whether the encode pipeline runs the audio-only or full video template. |
| mediaId required | string The id of an already-created media (still in UPLOADING) to upload the source for |
{- "contentType": "string",
- "mediaId": "string"
}Allows selected update operations on Media items such as: Adding new translation languages, change the title, move to trash.
| mediaId required | string mediaId |
updateMediaDto
| moveToTrash | boolean Moves the media to Trash |
| restore | boolean Restore a media that was moved to Trash |
| title | string The new Media Title |
object (LanguageSelectionDto) |
{- "moveToTrash": true,
- "restore": true,
- "title": "string",
- "translationLanguage": {
- "languageId": "string",
- "type": "CAPTION"
}
}Change the language of terms or translations in a term base
| termBaseId required | string termBaseId |
| workspaceId required | string workspaceId |
| fromLanguageId required | string The ID of the language to change from |
| toLanguageId required | string The ID of the language to change to |
Create or update a term in a term base
| termBaseId required | string termBaseId |
| workspaceId required | string workspaceId |
term
| dateCreated | string <date-time> The term creation date |
| dateDeleted | string <date-time> The date this term was deleted |
| id | string |
object (Language) | |
| notes | string Additional notes about the term |
object (TermBase) | |
| text | string The text content of the term |
Array of objects (TermTranslation) unique The translations of this term |
{- "dateCreated": "2019-08-24T14:15:22Z",
- "dateDeleted": "2019-08-24T14:15:22Z",
- "id": "string",
- "language": {
- "cps": 0,
- "displayName": "string",
- "id": "string",
- "localizedName": "string"
}, - "notes": "string",
- "termBase": {
- "dateCreated": "2019-08-24T14:15:22Z",
- "dateDeleted": "2019-08-24T14:15:22Z",
- "id": "string",
- "media": {
- "captionPostEdit": true,
- "createdVia": "API",
- "dateCreated": "2019-08-24T14:15:22Z",
- "dateDeleted": "2019-08-24T14:15:22Z",
- "dateExpired": "2019-08-24T14:15:22Z",
- "duration": 0,
- "errorMessage": "string",
- "hasMultipleLanguages": true,
- "id": "string",
- "importedFromPremiere": true,
- "language": {
- "cps": 0,
- "displayName": "string",
- "id": "string",
- "localizedName": "string"
}, - "sourceMediaAvailable": true,
- "sourceUrl": "string",
- "speakerLabels": "string",
- "status": "ENCODED",
- "title": "string",
- "tracks": [
- {
- "dateApproved": "2019-08-24T14:15:22Z",
- "dateCreated": "2019-08-24T14:15:22Z",
- "id": "string",
- "isPartial": true,
- "jobType": "CAPTION",
- "language": {
- "cps": 0,
- "displayName": "string",
- "id": "string",
- "localizedName": "string"
}, - "lastModified": "2019-08-24T14:15:22Z",
- "sourceTrack": { },
- "status": "COMPLETE"
}
], - "type": "AUDIO",
- "user": {
- "billingVersion": 0,
- "credits": 0,
- "creditsTopUp": 0,
- "email": "string",
- "firstName": "string",
- "id": "string",
- "lastName": "string",
- "mediaCount": 0,
- "userSubscription": "DEMO"
}, - "workspace": {
- "accessMode": "PRIVATE",
- "currentUserCanUpload": true,
- "currentUserRole": "COLLABORATOR",
- "dateCreated": "2019-08-24T14:15:22Z",
- "id": "string",
- "isRootWorkspace": true,
- "name": "string",
- "owner": {
- "billingVersion": 0,
- "credits": 0,
- "creditsTopUp": 0,
- "email": "string",
- "firstName": "string",
- "id": "string",
- "lastName": "string",
- "mediaCount": 0,
- "userSubscription": "DEMO"
}
}
}, - "terms": [
- { }
], - "workspaceId": "string"
}, - "text": "string",
- "translations": [
- {
- "dateCreated": "2019-08-24T14:15:22Z",
- "dateDeleted": "2019-08-24T14:15:22Z",
- "id": "string",
- "language": {
- "cps": 0,
- "displayName": "string",
- "id": "string",
- "localizedName": "string"
}, - "term": { },
- "text": "string"
}
]
}Add multiple source-language terms in a single request. Silently skips duplicates.
| termBaseId required | string termBaseId |
| workspaceId required | string workspaceId |
dto
| languageId | string |
| terms | Array of strings |
{- "languageId": "string",
- "terms": [
- "string"
]
}This namespace contains endpoints to export subtitle tracks and to import corrected subtitles into an existing caption track.
Export the tracks specified in input in bulk, as a single Zip file.
downloadTracksDto
| format | string Enum: "DOCX" "ITT" "PDF" "SBV" "SRT" "SSA" "STL" "TTML" "TXT" "VTT" |
| trackIds | Array of strings |
| workspaceId | string |
{- "format": "DOCX",
- "trackIds": [
- "string"
], - "workspaceId": "string"
}Returns a single subtitle track: language, job type, status, and timestamps, in the same shape as the 'tracks' entries embedded in a media object. Webhook 'links.track' URLs point here. Responds 404 when the track does not exist, was deleted, or is not readable by your account.
| trackId required | string trackId |
Returns a time-limited download URL (as a plain string body, not the video bytes) for a COMPLETE burn-in of this track. Responds 400 while the burn-in is still processing.
| burninId required | string burninId |
| trackId required | string trackId |
Starts an asynchronous job that burns the track's subtitles into the video, producing an MP4 (H.264) with hardcoded subtitles. This is what the web app calls an MP4 export. 'videoFormat' selects the output resolution: 'STANDARD' renders HD-ready 720p, 'HIGH' renders Full HD 1080p, 'HIGHEST' renders Ultra HD 4K, and 'ORIGINAL' keeps the source resolution (up to 4K). Formats above the source resolution upscale. The request body is optional: each style field you send overrides the track's saved style (the look shown in the web editor, seeded from the workspace defaults), and omitted fields, an empty {} body, or no body at all render the track's saved style unchanged. Responds 201 with the burn-in job, 409 with {"error", "existingBurnInId"} when one is already running for this track, 403 for free-tier accounts or missing edit permission, and 400 while the track is still processing. Track when it finishes via the 'burnin.completed' webhook or by polling 'Read Burn-Ins'.
| trackId required | string trackId |
| videoFormat required | string Enum: "HIGH" "HIGHEST" "ORIGINAL" "STANDARD" videoFormat |
| diarization | string Enum: "DASH" "LABEL" "NONE" diarization |
styleDto
| backgroundColor | string |
| backgroundFormat | string Enum: "DROPSHADOW" "LETTERBOX" "NONE" "OUTLINE" |
| backgroundOpacity | integer <int32> |
| fontColor | string |
| fontFace | string |
| fontSize | integer <int32> |
{- "backgroundColor": "string",
- "backgroundFormat": "DROPSHADOW",
- "backgroundOpacity": 0,
- "fontColor": "string",
- "fontFace": "string",
- "fontSize": 0
}Export a track in the format specified in input. On free-tier workspaces exports respond 402 (upgrade required) and any permitted output is watermarked.
| format required | string Enum: "DOCX" "ITT" "PDF" "SBV" "SRT" "SSA" "STL" "TTML" "TXT" "VTT" format |
| trackId required | string trackId |
| diarization | string Enum: "DASH" "LABEL" "NONE" diarization |
| offsetMs | integer <int64> Default: 0 offsetMs |
Replaces the subtitles of an existing CAPTION track with the inline SRT in the request body, and (with clearTranslations) regenerates its translations.
Typical use: after correcting an AI transcription outside Fluen, re-import it here. The SRT is validated and applied in a single call; an invalid SRT is rejected with 422 and nothing is written.
Parameters
validateOnly (default false): validate the SRT without writing it, a dry run, e.g. to pre-check a batch before committing any of it.clearTranslations (default false): clear the content of all the media's translation tracks and park them until the caption is completed.autoComplete (default false, requires clearTranslations=true): mark the caption COMPLETE after the import and start all translations immediately. Re-run translations are billed like any translation. Under heavy translation load the response returns translationsDeferred=true and the translations stay parked; retry the same call later.Responses
404: unknown or inaccessible track422: invalid SRT content402: free-tier workspace| trackId required | string trackId |
| autoComplete | boolean Default: false autoComplete |
| clearTranslations | boolean Default: false clearTranslations |
| validateOnly | boolean Default: false Validate the SRT without writing it (dry run). Defaults to false: the import is applied. |
body
| srtContent required | string The inline SRT content that will replace the track's subtitles |
{- "srtContent": "string"
}Returns the endpoint including its signing secret. The secret is shown only in this response (and after rotate-secret); use it to verify the X-Fluen-Signature header on deliveries. Each user can have at most 50 active endpoints; past that, creation responds 400.
| authenticated | boolean |
| authorities[0].authority | string |
| credentials | object |
| details | object |
| principal | object |
request
| eventTypes | Array of strings Event type codes to subscribe to |
| isActive | boolean Update only: true re-enables a disabled endpoint (resets its failure counter), false pauses it |
| label | string Optional display label |
| url | string HTTPS URL that will receive event POSTs |
| workspaceId | string Optional workspace id; when set, only events for media in this workspace are delivered |
{- "eventTypes": [
- "media.ready",
- "caption.completed",
- "translation.completed"
], - "isActive": true,
- "label": "string",
- "workspaceId": "string"
}Returns up to 3 recent real events of the requested type for your account (newest first), in the exact shape delivered to endpoints. Falls back to one canned example when you have no such event yet. Powers Zapier's sample-data step at Zap-setup time.
| authenticated | boolean |
| authorities[0].authority | string |
| credentials | object |
| details | object |
| principal | object |
| type required | string Example: type=translation.completed Event type code |
| endpointId required | string endpointId |
| authenticated | boolean |
| authorities[0].authority | string |
| credentials | object |
| details | object |
| principal | object |
request
| eventTypes | Array of strings Event type codes to subscribe to |
| isActive | boolean Update only: true re-enables a disabled endpoint (resets its failure counter), false pauses it |
| label | string Optional display label |
| url | string HTTPS URL that will receive event POSTs |
| workspaceId | string Optional workspace id; when set, only events for media in this workspace are delivered |
{- "eventTypes": [
- "media.ready",
- "caption.completed",
- "translation.completed"
], - "isActive": true,
- "label": "string",
- "workspaceId": "string"
}| endpointId required | string endpointId |
| authenticated | boolean |
| authorities[0].authority | string |
| credentials | object |
| details | object |
| page | integer <int32> Default: 0 0-indexed page |
| principal | object |
| size | integer <int32> Default: 20 size |
| endpointId required | string endpointId |
| authenticated | boolean |
| authorities[0].authority | string |
| credentials | object |
| details | object |
| principal | object |
Synchronous: reports whether your receiver answered 2xx. Use it to verify the URL and your signature check.
| endpointId required | string endpointId |
| authenticated | boolean |
| authorities[0].authority | string |
| credentials | object |
| details | object |
| principal | object |