Skip to content

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#

bash
npm i @videxo/sdk

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.

JavaScript
import { Videxo } from '@videxo/sdk';

const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });

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:

OptionDefaultMeaning
apiKeyrequiredAn API key from the dashboard.
baseUrlhttps://api.videxo.ioThe API's address.
fetchthe runtime's fetchThe fetch to send requests with.
maxRetries2Retries of a failed request (below).
timeoutMs60000How 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:

JavaScript
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);
}

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:

JavaScript
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);

jobs.create() takes an operations body with several outputs, or a command body:

JavaScript
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}}',
});

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:

JavaScript
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' });

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#

JavaScript
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);

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:

JavaScript
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));

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.

JavaScript
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);

For a large output, stream the body to a file instead of holding it in memory:

JavaScript
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'));

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:

JavaScript
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' });

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.

JavaScript
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;
}

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:

CodeMeaning
missing_headerswebhook-id, webhook-timestamp or webhook-signature is missing.
invalid_secretThe secret is not a whsec_ signing secret.
invalid_timestampThe timestamp is not a number or is more than five minutes away (toleranceSeconds).
invalid_signatureNo v1 signature matches the body.
invalid_payloadThe signed body is not a webhook event.
JavaScript
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;
  }
}

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:

JavaScript
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);
}

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:

CodeMeaning
network_errorThe request got no response: DNS, TLS or a dropped connection.
timeoutNo whole response within timeoutMs.
abortedYour AbortSignal stopped the request or the wait.
wait_timeoutjobs.wait() gave up before the job ended; body holds the job as last read.
http_errorAn error response that is not problem details, such as a proxy's page; status is set.
invalid_responseA successful response the SDK could not read.
output_not_foundoutputs.url() or outputs.download() named an output the job does not have.
output_ambiguousThe 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.

CallAPI requestResult
new Videxo(options)noneA client (options).
transcode(body, options?)POST /v1/transcodePromise<Job>, queued.
thumbnails(body, options?)POST /v1/thumbnailsPromise<Job>, queued.
storyboard(body, options?)POST /v1/storyboardPromise<Job>, queued.
gif(body, options?)POST /v1/gifPromise<Job>, queued.
audio(body, options?)POST /v1/audioPromise<Job>, queued.
concat(body, options?)POST /v1/concatPromise<Job>, queued.
slideshow(body, options?)POST /v1/slideshowPromise<Job>, queued.
probe(body, options?)POST /v1/probe, waiting for the jobPromise<ProbeResponse>, the job and probe, the document (Read media metadata).
scenes(body, options?)POST /v1/scenesPromise<Job>, queued.
jobs.create(body, options?)POST /v1/jobs, an operations body or a command bodyPromise<Job>, queued.
jobs.compile(body, options?)POST /v1/jobs/compilePromise<CompileResult>.
jobs.get(id, options?)GET /v1/jobs/{id}Promise<Job>.
jobs.page(params?, options?)GET /v1/jobs with limit, state and cursorPromise<JobList>: data and next_cursor.
jobs.list(params?, options?)GET /v1/jobs, page after pageAn async iterator of Job, newest first.
jobs.wait(id, options?)GET /v1/jobs/{id}, repeatedPromise<Job> in succeeded, failed or cancelled.
jobs.cancel(id, options?)POST /v1/jobs/{id}/cancelPromise<Job>.
jobs.deleteFiles(id, options?)DELETE /v1/jobs/{id}/filesPromise<Job>, each output with deleted_at set and no links.
presets.list(options?)GET /v1/presetsPromise<PresetList>.
uploads.create(params, options?)POST /v1/uploads with filename, size_bytes and content_typePromise<Upload>.
uploads.upload(data, options)POST /v1/uploads, then a PUT of the bytesPromise<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 linkPromise<Response> with the file.
webhooks.verify(body, headers, secret, options?)nonePromise<WebhookEvent>.
verifyWebhook(body, headers, secret, options?)nonePromise<WebhookEvent>.
sendUpload(uploadUrl, data, options?)a PUT of the bytes to upload_urlPromise<CompletedUpload>.
clientTokens.create(grant, options?)POST /v1/client-tokensPromise<ClientToken>: id, token, chain_id, expires_at and grant.
clientTokens.forUpload(options)POST /v1/client-tokens for one upload by subjectPromise<ClientToken>.
clientTokens.forJob(jobId, options?)POST /v1/client-tokens to read one job, new or with continuesPromise<ClientToken>.
clientTokens.revoke(id, options?)DELETE /v1/client-tokens/{id}Promise<void>.
clientTokens.revokeSubject(subject, options?)POST /v1/client-tokens/revokePromise<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:

CallAPI requestResult
new VidexoClient({ token, tokenId?, baseUrl?, getToken? })noneA 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 retriesPromise<Upload>.
uploads.upload(data, { filename, onProgress?, ... })POST /v1/client/uploads, then a PUT of the bytesPromise<CompletedUpload>.
jobs.get(id, options?)GET /v1/client/jobs/{id}Promise<ClientJob>.
jobs.list(options?)GET /v1/client/jobsPromise<ClientJob[]>, newest first.
jobs.wait(id, options?)GET /v1/client/jobs/{id}, repeatedPromise<ClientJob> in succeeded, failed or cancelled.
token(options?)GET /v1/client/tokenPromise<ClientTokenInfo>.

The options:

OptionsOfFields
Requestevery API callsignal: an AbortSignal that stops the call. timeoutMs: this call's timeout per attempt.
Createthe operation methods and jobs.create()The request options and idempotencyKey.
Waitjobs.wait()timeoutMs for the whole wait (10 minutes by default), intervalMs between reads (2 seconds), signal.
Uploaduploads.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 uploadsendUpload()contentType, fetch, signal, timeoutMs (none by default).
Verifywebhooks.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.