DocsGet started
TypeScript SDK
Install @videxo/sdk from npm and call the API from TypeScript: jobs, operation endpoints, uploads, downloads, webhooks, errors and retries.
@videxo/sdk is a small TypeScript client of the API. Its types are generated from the API's
OpenAPI document, so a request body is checked by your compiler. It sends
requests with fetch and verifies webhooks with WebCrypto.
Install#
npm i @videxo/sdkCopy failed. The text is selected; copy it with your keyboard.
pnpm add @videxo/sdkCopy failed. The text is selected; copy it with your keyboard.
yarn add @videxo/sdkCopy failed. The text is selected; copy it with your keyboard.
bun add @videxo/sdkCopy failed. The text is selected; copy it with your keyboard.
It is an ES module with its own type declarations. It runs on Node 20 or newer, Bun, Deno and Cloudflare Workers. In a browser, use it only to send a file to an upload URL (below). Prefer plain HTTP? The API reference shows every request the SDK sends. For an iOS or macOS app, see the Swift SDK.
Create a client#
The API key belongs on your server. Never put it in browser code: anyone who opens the page can read it. In a browser the SDK only sends files to an upload URL your server made (below), or uses a client token your server minted.
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });Copy failed. The text is selected; copy it with your keyboard.
The constructor trims the key and throws a TypeError when it is then empty or has a character
other than printable ASCII without spaces, and when baseUrl does not start with http:// or
https:// (a trailing / is removed). It does not check the key with the API; the first request
does. Options:
| Option | Default | Meaning |
|---|---|---|
apiKey | required | An API key from the dashboard. |
baseUrl | https://api.videxo.io | The API's address. |
fetch | the runtime's fetch | The fetch to send requests with. |
maxRetries | 2 | Retries of a failed request (below). |
timeoutMs | 60000 | How long one attempt may take, until its whole response arrives (for a download, until its headers arrive). |
Create a job and wait for it#
One method per operation endpoint: transcode, thumbnails, storyboard, gif, audio,
concat, slideshow and scenes. Each takes the endpoint's body and returns the job, queued:
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const job = await videxo.transcode({ input: 'https://example.com/talk.mp4', preset: 'web-720p' });
const done = await videxo.jobs.wait(job.id);
if (done.state === 'succeeded') {
for (const output of done.outputs) console.log(output.name, output.filename, output.download_url);
} else {
console.log(done.state, done.error?.code, done.error?.message);
}Copy failed. The text is selected; copy it with your keyboard.
probe() reads a file's container, duration and streams and waits for the job itself. A job that
failed is answered with probe set to null, not thrown:
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const { probe, error } = await videxo.probe({ input: 'https://example.com/talk.mp4' });
if (probe) console.log(probe.duration, probe.streams.map((stream) => stream.codec));
else console.log(error?.code, error?.reason);Copy failed. The text is selected; copy it with your keyboard.
jobs.create() takes an operations body with several outputs, or a command body:
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
await videxo.jobs.create({
inputs: { talk: 'https://example.com/talk.mp4' },
outputs: [
{ name: 'web', preset: 'web-720p' },
{ name: 'poster', preset: 'poster', at: 3 },
],
metadata: { order: '1234' },
});
await videxo.jobs.create({
input_files: { in_video: 'https://example.com/talk.mp4' },
output_files: { out_main: 'clip.mp4' },
command: '-i {{in_video}} -vf scale=-2:720 -c:v libx264 -crf 23 {{out_main}}',
});Copy failed. The text is selected; copy it with your keyboard.
Every creation sends an Idempotency-Key: a new random one per call, reused on
the SDK's own retries. Pass your own to make your retries safe too, such as after a restart:
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
await videxo.gif({ input: 'https://example.com/talk.mp4' }, { idempotencyKey: 'order-1234-gif' });Copy failed. The text is selected; copy it with your keyboard.
jobs.wait(id, { timeoutMs, intervalMs, signal }) reads the job every 2 seconds until it is
succeeded, failed or cancelled, for at most 10 minutes by default, and waits out a 429 by
its Retry-After. It returns a failed job instead of throwing: read job.error. A webhook saves
the polling (below).
Read jobs#
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const job = await videxo.jobs.get('job_3k9x0c2mfq8w1n7r5t4b6v2z');
for await (const failed of videxo.jobs.list({ state: 'failed' })) console.log(failed.id, failed.error?.code);
const page = await videxo.jobs.page({ limit: 20 });
console.log(page.data.length, page.next_cursor);
await videxo.jobs.cancel(job.id);
await videxo.jobs.deleteFiles(job.id);Copy failed. The text is selected; copy it with your keyboard.
jobs.list() reads the next page only when the loop gets there; jobs.page() returns one page,
whose next_cursor you pass as cursor. Both take limit (1 to 100, 20 by default), state, created_after, created_before and
cursor.
jobs.cancel() returns the job, cancelled or as it already ended. When the API cannot confirm the
cancellation, it throws a VidexoError with status 503: read the job with jobs.get() before you
try again, as it may already have finished or been cancelled. jobs.deleteFiles() returns the job
with deleted_at set on each output and its links null.
Compile and presets#
jobs.compile() returns the spec, the command body and the ffmpeg_command a body would get,
without making a job (POST /v1/jobs/compile). presets.list() returns every preset
with its full options:
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const compiled = await videxo.jobs.compile({ operation: 'thumbnails', input: 'https://example.com/talk.mp4', times: [5, 30] });
console.log(compiled.ffmpeg_command);
const { data: presets } = await videxo.presets.list();
console.log(presets.map((preset) => preset.name));Copy failed. The text is selected; copy it with your keyboard.
Download the outputs#
outputs.download(jobId, output) returns the file as a Response; outputs.url() returns a fresh
signed link to hand on. output is the output's alias (out_video), or its name or file name
when only one file of the job has it. The API key is not sent to the file's link, and the
signal you pass stops the download until its body is read.
import { writeFile } from 'node:fs/promises';
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const response = await videxo.outputs.download('job_3k9x0c2mfq8w1n7r5t4b6v2z', 'video');
await writeFile('video.mp4', new Uint8Array(await response.arrayBuffer()));
const link = await videxo.outputs.url('job_3k9x0c2mfq8w1n7r5t4b6v2z', 'out_video');
console.log(link);Copy failed. The text is selected; copy it with your keyboard.
For a large output, stream the body to a file instead of holding it in memory:
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const response = await videxo.outputs.download('job_3k9x0c2mfq8w1n7r5t4b6v2z', 'video');
if (!response.body) throw new Error('The download has no body.');
await pipeline(Readable.fromWeb(/** @type {import('node:stream/web').ReadableStream} */ (response.body)), createWriteStream('video.mp4'));Copy failed. The text is selected; copy it with your keyboard.
Upload a file#
uploads.upload() creates a one-time upload and sends the bytes to it (Files). It
takes a File, a Blob, an ArrayBuffer, a Uint8Array or a ReadableStream (with size), and
returns the completed upload, whose input_url a job takes as an input:
import { readFile } from 'node:fs/promises';
import { Videxo } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
const upload = await videxo.uploads.upload(await readFile('talk.mp4'), { filename: 'talk.mp4', contentType: 'video/mp4' });
await videxo.transcode({ input: upload.input_url, preset: 'web-720p' });Copy failed. The text is selected; copy it with your keyboard.
Upload from a browser#
Your server creates the upload with its key and gives the browser only the upload_url; the
browser sends the file there with sendUpload(), which needs no key. The URL works once. Send a
File, a Blob or bytes from a browser; a ReadableStream is for server code.
import { Videxo, sendUpload } from '@videxo/sdk';
// On your server, with the name, size and type the browser sent of the file the user picked:
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
/** @param {{ name: string, size: number, type: string }} picked */
async function reserve(picked) {
const created = await videxo.uploads.create({ filename: picked.name, size_bytes: picked.size, content_type: picked.type });
return created.upload_url;
}
// In the browser, with the upload_url your server returned for that same file:
/** @param {File} file @param {string} uploadUrl */
async function send(file, uploadUrl) {
const upload = await sendUpload(uploadUrl, file, { contentType: file.type });
return upload.input_url;
}Copy failed. The text is selected; copy it with your keyboard.
Verify a webhook#
webhooks.verify(body, headers, secret, options?) checks a delivery's signature
and returns its event. Pass the raw body as it arrived (a string, Uint8Array or ArrayBuffer),
before any JSON parser reads it, and the request's headers as Headers or a plain object. It
throws a VidexoWebhookError; respond to those with 400 and do not act on them. Its code says
why:
| Code | Meaning |
|---|---|
missing_headers | webhook-id, webhook-timestamp or webhook-signature is missing. |
invalid_secret | The secret is not a whsec_ signing secret. |
invalid_timestamp | The timestamp is not a number or is more than five minutes away (toleranceSeconds). |
invalid_signature | No v1 signature matches the body. |
invalid_payload | The signed body is not a webhook event. |
import { Videxo, VidexoWebhookError } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
/** @param {Request} request */
export async function handleWebhook(request) {
try {
const event = await videxo.webhooks.verify(await request.text(), request.headers, process.env.VIDEXO_WEBHOOK_SECRET ?? '');
console.log(event.type, event.data.id, event.data.state);
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof VidexoWebhookError) return new Response(error.code, { status: 400 });
throw error;
}
}Copy failed. The text is selected; copy it with your keyboard.
verifyWebhook(body, headers, secret, options?) is the same function without a client, for code
that has no API key.
Errors#
Every failed API call throws a VidexoError. Programming errors throw a TypeError before any
request: the constructor's checks above, and uploads.upload() with a stream but no size. An
error response of the API carries its problem details: status, code, reason
(the finer case, or null), title, detail, type, retryable (whether the same request can
succeed later), requestId (quote it to support), errors (with invalid_request), problems
(with command_invalid), retryAfter and the raw body. Programs branch on code, and on
reason when they need the finer case:
import { Videxo, VidexoError } from '@videxo/sdk';
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
try {
await videxo.transcode({ input: 'https://example.com/talk.mp4', fade: { out: 1 } });
} catch (error) {
if (!(error instanceof VidexoError)) throw error;
if (error.code === 'invalid_request') for (const field of error.errors) console.log(field.pointer, field.detail);
else console.log(error.status, error.code, error.reason, error.detail, error.requestId);
}Copy failed. The text is selected; copy it with your keyboard.
When no problem details response came, status is null (or the status of a response that was not
problem details), reason and retryable are null, requestId is the Request-Id header when a
response came, and code is one of the SDK's own:
| Code | Meaning |
|---|---|
network_error | The request got no response: DNS, TLS or a dropped connection. |
timeout | No whole response within timeoutMs. |
aborted | Your AbortSignal stopped the request or the wait. |
wait_timeout | jobs.wait() gave up before the job ended; body holds the job as last read. |
http_error | An error response that is not problem details, such as a proxy's page; status is set. |
invalid_response | A successful response the SDK could not read. |
output_not_found | outputs.url() or outputs.download() named an output the job does not have. |
output_ambiguous | The name belongs to several files of the job (thumbnails at several times); use the alias. |
The message of an error never holds the API key.
Retries and timeouts#
A request is tried again after 429, 502, 503 or 504, a timeout or a network error, at most
maxRetries times, with a growing pause and jitter between attempts. A Retry-After header
(seconds or a date) sets the pause; one over a minute is not waited for, and the error is thrown.
Other responses are never retried.
Only a request that is safe to send twice is retried that way: reads, cancel, compile, deleting
files, and every creation, which carries its Idempotency-Key. uploads.create() has no key, so
it is retried only after a 429, which the API sends before doing anything. The bytes sent to an
upload URL are never sent twice.
Every call to the API takes { signal, timeoutMs } in its last argument, to stop it or give it
more time; jobs.wait() takes its own timeoutMs for the whole wait.
Reference#
Every method that calls the API takes its options last. The bodies and results are the API's own objects; the API reference lists their fields.
| Call | API request | Result |
|---|---|---|
new Videxo(options) | none | A client (options). |
transcode(body, options?) | POST /v1/transcode | Promise<Job>, queued. |
thumbnails(body, options?) | POST /v1/thumbnails | Promise<Job>, queued. |
storyboard(body, options?) | POST /v1/storyboard | Promise<Job>, queued. |
gif(body, options?) | POST /v1/gif | Promise<Job>, queued. |
audio(body, options?) | POST /v1/audio | Promise<Job>, queued. |
concat(body, options?) | POST /v1/concat | Promise<Job>, queued. |
slideshow(body, options?) | POST /v1/slideshow | Promise<Job>, queued. |
probe(body, options?) | POST /v1/probe, waiting for the job | Promise<ProbeResponse>, the job and probe, the document (Read media metadata). |
scenes(body, options?) | POST /v1/scenes | Promise<Job>, queued. |
jobs.create(body, options?) | POST /v1/jobs, an operations body or a command body | Promise<Job>, queued. |
jobs.compile(body, options?) | POST /v1/jobs/compile | Promise<CompileResult>. |
jobs.get(id, options?) | GET /v1/jobs/{id} | Promise<Job>. |
jobs.page(params?, options?) | GET /v1/jobs with limit, state and cursor | Promise<JobList>: data and next_cursor. |
jobs.list(params?, options?) | GET /v1/jobs, page after page | An async iterator of Job, newest first. |
jobs.wait(id, options?) | GET /v1/jobs/{id}, repeated | Promise<Job> in succeeded, failed or cancelled. |
jobs.cancel(id, options?) | POST /v1/jobs/{id}/cancel | Promise<Job>. |
jobs.deleteFiles(id, options?) | DELETE /v1/jobs/{id}/files | Promise<Job>, each output with deleted_at set and no links. |
presets.list(options?) | GET /v1/presets | Promise<PresetList>. |
uploads.create(params, options?) | POST /v1/uploads with filename, size_bytes and content_type | Promise<Upload>. |
uploads.upload(data, options) | POST /v1/uploads, then a PUT of the bytes | Promise<CompletedUpload>. |
outputs.url(jobId, output, options?) | GET /v1/jobs/{id}/outputs/{alias} | Promise<string>, a fresh signed link. |
outputs.download(jobId, output, options?) | outputs.url(), then a GET of the link | Promise<Response> with the file. |
webhooks.verify(body, headers, secret, options?) | none | Promise<WebhookEvent>. |
verifyWebhook(body, headers, secret, options?) | none | Promise<WebhookEvent>. |
sendUpload(uploadUrl, data, options?) | a PUT of the bytes to upload_url | Promise<CompletedUpload>. |
clientTokens.create(grant, options?) | POST /v1/client-tokens | Promise<ClientToken>: id, token, chain_id, expires_at and grant. |
clientTokens.forUpload(options) | POST /v1/client-tokens for one upload by subject | Promise<ClientToken>. |
clientTokens.forJob(jobId, options?) | POST /v1/client-tokens to read one job, new or with continues | Promise<ClientToken>. |
clientTokens.revoke(id, options?) | DELETE /v1/client-tokens/{id} | Promise<void>. |
clientTokens.revokeSubject(subject, options?) | POST /v1/client-tokens/revoke | Promise<number>, how many tokens were revoked. |
The separate entry point @videxo/sdk/client is for apps and browsers, with a
client token instead of an API key:
| Call | API request | Result |
|---|---|---|
new VidexoClient({ token, tokenId?, baseUrl?, getToken? }) | none | A client; it refuses a value shaped like an API key. |
uploads.create(params, options?) | POST /v1/client/uploads with an Idempotency-Key it keeps for its retries | Promise<Upload>. |
uploads.upload(data, { filename, onProgress?, ... }) | POST /v1/client/uploads, then a PUT of the bytes | Promise<CompletedUpload>. |
jobs.get(id, options?) | GET /v1/client/jobs/{id} | Promise<ClientJob>. |
jobs.list(options?) | GET /v1/client/jobs | Promise<ClientJob[]>, newest first. |
jobs.wait(id, options?) | GET /v1/client/jobs/{id}, repeated | Promise<ClientJob> in succeeded, failed or cancelled. |
token(options?) | GET /v1/client/token | Promise<ClientTokenInfo>. |
The options:
| Options | Of | Fields |
|---|---|---|
| Request | every API call | signal: an AbortSignal that stops the call. timeoutMs: this call's timeout per attempt. |
| Create | the operation methods and jobs.create() | The request options and idempotencyKey. |
| Wait | jobs.wait() | timeoutMs for the whole wait (10 minutes by default), intervalMs between reads (2 seconds), signal. |
| Upload | uploads.upload() | filename (required), contentType (the blob's type or application/octet-stream), size (required for a stream), uploadTimeoutMs for sending the bytes (none by default), and the request options, whose timeoutMs applies to creating the upload. |
| Send upload | sendUpload() | contentType, fetch, signal, timeoutMs (none by default). |
| Verify | webhooks.verify(), verifyWebhook() | toleranceSeconds, how far the timestamp may be from now (300 by default). |
The bytes sent to an upload URL are never sent twice, as the URL works once. When sendUpload() or
uploads.upload() fails while sending, create a new upload and send the file again.
The SDK has no methods for GET /v1/me, webhook endpoints, the signing secret or webhook
deliveries yet; call those over HTTP as the API reference shows.