@transcript1/sdk
v1.5.0
Published
TypeScript SDK for transcribing videos from YouTube, X, TikTok, Instagram, Facebook, Bluesky, Twitch, Dailymotion, Pinterest, LinkedIn and Snapchat, plus audio/video files, with transcript1.
Readme
@transcript1/sdk
TypeScript SDK for the transcript1 transcription API. Transcribe supported public video links or upload audio and video files from server code. Download transcripts as text, SRT, VTT or JSON.
Full documentation: https://transcript1.com/docs/sdk
Install
npm i @transcript1/sdkNode 20.19+. Bun and Deno should work through their node: compatibility; the automated tests run on Node only. Use
the SDK server-side only; never ship an API key to a browser.
The examples use ES modules. Save them as .mjs files, or set "type": "module" in package.json for .js files.
require() also works on Node 20.19+.
API key
The client reads your API key from TRANSCRIPT1_API_KEY. You can also pass it to the constructor:
import { Transcript1 } from '@transcript1/sdk';
const t1 = new Transcript1(); // uses TRANSCRIPT1_API_KEY
const other = new Transcript1({ apiKey: 't1_...' });Without a key the constructor throws a Transcript1Error with the code missing_api_key.
Transcribe
transcribe creates a job and waits for its transcript. It returns a completed transcription with a transcript
field. Failed or canceled jobs reject.
const done = await t1.transcribe('https://youtu.be/dQw4w9WgXcQ'); // YouTube link
console.log(done.transcript.text);
await t1.transcribe('./interview.m4a', { speakers: true }); // file path
await t1.transcribe(buffer, { filename: 'call.mp3' }); // Buffer or Uint8Array
await t1.transcribe(blob, { filename: 'call.mp3' }); // Blob (a File brings its own name)
await t1.transcribe(stream, { filename: 'call.mp3', size: 4_200_000 }); // stream or async iterableStrings starting with http:// or https:// are links; other strings are file paths. Use public video links from
YouTube, X, TikTok, Instagram, Facebook, Bluesky, Twitch, Dailymotion, Pinterest, LinkedIn and Snapchat or upload audio and video files.
The additional sources accept HTTPS links to completed Twitch VODs/clips, Dailymotion videos, native Pinterest video pins,
LinkedIn public video posts and Snapchat Spotlight. No supplied account cookies, private/login-required content, live streams
or collections. Pinterest selects its first native video block, not external embeds. See the exact accepted hosts and paths
in Sources and options. URL time parameters do not trim media.
Twitch, Pinterest and Snapchat use auto or stt, not captions. Keep returned media_id strings opaque, including Twitch clip and Spotlight IDs.
SDK 1.1.0 and older reject Twitch, Dailymotion, Pinterest, LinkedIn and Snapchat links locally; use 1.2.0 or newer.
Files can be up to 2,000,000,000 bytes, with extensions mp3, wav, m4a, flac, ogg, opus, mp4, mov, webm or mkv.
Download
download waits for the job if needed and returns a string.
const text = await t1.download(done.id); // plain text
const withTimes = await t1.download(done.id, { timestamps: true }); // text only
const srt = await t1.download(done.id, { format: 'srt' });
const vtt = await t1.download(done.id, { format: 'vtt' });
const json = await t1.download(done.id, { format: 'json' }); // the API's JSON texttimestamps: true with a format other than text rejects with invalid_input.
Background jobs
const job = await t1.transcriptions.create('./long-interview.mp4'); // returns once the job is queued
const now = await t1.transcriptions.get(job.id); // current state, at once
const finished = await t1.transcriptions.get(job.id, { wait: true }); // until it endsget(id, { wait: true }) returns the finished object whether its status is completed, failed or canceled.
Check the status before reading its transcript. Pass includeWords: true to request word timings when available.
YouTube and X, plus TikTok, Bluesky, Dailymotion and LinkedIn, use native captions when a usable track is available
with source: 'auto', falling back to AI transcription. source: 'captions' is supported on these six platforms
and never falls back to AI. Other platforms use AI transcription; Facebook caption retrieval remains unproven and is not enabled.
Dailymotion supports direct SRT, not caption playlists. Bluesky captions require a supported trusted PDS (not arbitrary
federated hosts). LinkedIn supports public native posts, not LinkedIn Learning.
Speaker labels require AI transcription. Captions and AI transcription have the same per-minute price.
transcript_source is youtube_captions, x_captions, tiktok_captions, bluesky_captions,
dailymotion_captions, linkedin_captions or speech_to_text.
caption_kind is manual, auto_generated or null. Null can mean native metadata does not establish authorship;
it does not imply manual captions. Dailymotion en-auto/fr-auto tracks are auto-generated.
X and the four new native caption paths have segment timing only, with words: null, and no speaker labels.
source: 'stt' and speaker identification bypass captions; source: 'captions' cannot be combined with speakers or uploads.
List, delete, account
for await (const item of t1.transcriptions.list({ status: 'completed', limit: 50 })) {
console.log(item.id, item.created_at);
}
await t1.transcriptions.delete(job.id); // cancels a running job, deletes a finished one
const account = await t1.account();list fetches pages as you iterate. Params: status, platform, limit (page size, 1 to 100, API default 20) and signal.
Filter by platform with youtube, x, tiktok, instagram, facebook, bluesky, twitch, dailymotion, pinterest, linkedin, snapchat or upload.
Options
The constructor accepts apiKey, baseUrl, fetch and maxRetries (default 2). baseUrl defaults to
TRANSCRIPT1_API_URL when set, otherwise https://api.transcript1.com.
transcribe and transcriptions.create:
| Option | Meaning |
| --- | --- |
| speakers | label speakers |
| language | auto (default) or an ISO 639-1 code |
| translateTo | translate the transcript into this ISO 639-1 language (two lowercase letters) |
| source | auto, captions or stt; links only |
| fresh | links: skip reusing a result from the last 24 hours |
| filename | required for Buffers, Blobs without a name and streams |
| size | required for streams, in bytes |
| idempotencyKey | defaults to a new UUID per call |
| onProgress | progress callback |
| signal | an AbortSignal |
download: format (text, srt, vtt, json), timestamps, translation, signal.
transcriptions.get: wait, includeWords, translation, onProgress, signal. Progress events are reported while waiting.
translation is { mode: 'only' | 'both', language: 'de' }: segments carry the translation in that language next to the original text (both) or instead of it (only). Omit it to get the original text only (the SDK sends translation=none). A language the transcription has no translation for rejects with translation_not_found (404); a malformed language is invalid_request (422). Transcription.translations lists every translation ({ language, status }), oldest first.
Returned objects keep the API's field names (snake_case).
Translate
const job = await t1.transcribe('https://youtu.be/dQw4w9WgXcQ', { translateTo: 'pl' });
console.log(job.translation); // { language: 'pl', status: 'completed' }
console.log(job.transcript.segments[0].translation);
// translate an already completed transcription later (idempotent)
const { status } = await t1.transcriptions.translate(job.id, 'de');
const german = await t1.download(job.id, { format: 'srt', translation: { mode: 'only', language: 'de' } });translate resolves to { transcription_id, language, status }. It rejects with not_found (404), transcript_not_ready (409, the transcript is not completed yet) or invalid_request (422, bad language).
Live transcription
Stream raw PCM (16-bit, mono) and get subtitle lines back while people speak, optionally translated in real time. See Live transcription for the protocol, billing and limits.
const live = await t1.live.start({ language: 'en', translateTo: 'es', sampleRate: 16000 });
live.on('line', line => {
// the full current state of one line: replace it by line.id
console.log(line.id, line.text, line.translation?.text);
});
live.send(pcm); // Int16Array, Uint8Array or ArrayBuffer of 16-bit mono samples
const result = await live.end(); // { reason, transcriptionId, minutesCharged, connectedMs, audioMs }start()resolves once the session has started, so no event is missed.live.linesholds every line so far, in order.live.finalize()closes the current line, for example when the video pauses.- Other events:
started,usage,warning,reconnecting,reconnected,endedanderror.on()returns a function that removes the handler. - A dropped connection reconnects by itself within 2 minutes, with backoff from 0.5 to 8 seconds, and the last 10 seconds of audio are sent again. If
end()gets no answer from the server within 30 seconds, the SDK reads the session once and resolves with what it reports, or with reasondisconnected. t1.live.capabilities()lists the languages and translation targets;t1.live.get(id)reads a session.- The live client uses the built-in
WebSocket(Node 22+, Bun, Deno, browsers). On Node 20, pass one in:new Transcript1({ WebSocket }), for example from thewspackage.
In a browser
Never put an API key in a browser. Use the @transcript1/sdk/live entry with a short-lived OAuth access token instead; it is asked for again before every request, so it can be refreshed:
import { createLiveClient } from '@transcript1/sdk/live';
const live = createLiveClient({ getAccessToken: async () => getOauthAccessToken() });
const session = await live.start({ translateTo: 'de' });Progress
await t1.transcribe('./interview.m4a', {
onProgress(event) {
if (event.type === 'upload') {
console.log(`${event.sent} of ${event.total} bytes`);
} else if (event.type === 'status') {
console.log(event.status);
} else if (event.type === 'waiting_slot') {
console.log(`all job slots are busy, retrying in ${event.retryAfter} s`);
} else {
console.log('reconnecting');
}
},
});Abort
Pass an AbortSignal to set a time limit or cancel a call:
await t1.transcribe(url, { signal: AbortSignal.timeout(10 * 60_000) });Aborting before any request rejects with aborted and no id. During an upload, the SDK tries to delete the job if it is
still awaiting_upload, then rejects with aborted and the id. Aborting while waiting leaves the job running; the error
carries transcriptionId, so you can resume with get(id, { wait: true }).
Errors
Catch Transcript1Error to read API, job and SDK error codes:
import { Transcript1Error } from '@transcript1/sdk';
try {
await t1.transcribe('./call.mp3');
} catch (error) {
if (error instanceof Transcript1Error) {
console.error(error.code, error.message, error.transcriptionId);
}
}| Field | Meaning |
| --- | --- |
| code | API code (insufficient_minutes, not_found, ...), a job failure code (no_captions, no_speech, ...), canceled, or a local code |
| status | HTTP status; absent for job and local errors |
| details | extra fields of the API's error, e.g. minutes_needed, action_url |
| retryable | whether trying again can help |
| transcriptionId | the job, whenever one exists |
| transcription | the last object seen, when there is one |
Local codes: missing_api_key, invalid_input, aborted, network_error, upload_token_refused. A non-JSON error
body gives http_<status>.
A job that failed without an error object rejects with transcription_failed. If the API ever returns a completed job
without its transcript, transcribe rejects with internal_error.
transcribe resolves with a CompletedTranscription: a Transcription whose status is completed and whose
transcript is always present, so done.transcript.text type-checks.
Waiting and retries
While waiting for a job, the SDK retries dropped connections and HTTP 429, 500, 502, 503 and 504 responses without a
retry limit. Reconnection waits increase from 2 to 4, 8, 16 and then 30 seconds; use signal to bound the total time.
Other requests retry up to maxRetries times. When
your plan's active-job limit is reached, transcribe and transcriptions.create wait for a slot and retry.
Plans
See plans and prices at https://transcript1.com/#pricing.
