@transcodely/sdk
Official TypeScript / Node SDK for the Transcodely video transcoding API.
npm install @transcodely/sdk
Quick start
import { Transcodely, OutputFormat, VideoCodec, Resolution } from "@transcodely/sdk";
const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
// Create a job
const job = await client.jobs.create({
inputUrl: "https://example.com/source.mp4",
outputs: [{
type: OutputFormat.HLS,
video: [
{ codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_1080P },
{ codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_720P },
],
}],
});
console.log(job.id); // "job_a1b2c3d4e5f6"
// Watch progress in real time
for await (const event of client.jobs.watch(job.id)) {
console.log(event.job?.status, event.job?.progress);
if (event.job?.status === 4 /* COMPLETED */) break;
}
Authentication
Pass your API key in the constructor:
const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
Test-mode keys (ak_test_*) and live-mode keys (ak_live_*) hit the same base URL — the environment is encoded in the key prefix.
Resources
client.jobs // create / get / list / cancel / confirm / watch
client.videos // upload helpers, multipart, get / list / update / delete / watch
client.presets // create / get / getBySlug / list / update / duplicate / archive
client.origins // create / get / list / update / validate / archive
client.apps // create / get / list / update / archive / enableHosting
client.apiKeys // create / get / list / revoke
client.organizations // create / get / list / update / checkSlug
client.memberships // list / get / updateRole / remove
client.users // getMe / get / list / updateMe
client.health // check
Origins
An origin tells Transcodely where to read source media from and where to write outputs. Every origin belongs to a single provider; pass exactly one provider-config field (s3, gcs, http, or r2) on create.
Create an S3 origin
import { Transcodely, OriginPermission } from "@transcodely/sdk";
const origin = await client.origins.create({
name: "Production S3",
permissions: [OriginPermission.READ, OriginPermission.WRITE],
s3: {
bucket: "my-bucket",
region: "us-east-1",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
// endpoint: "https://s3.custom.example.com", // for MinIO, Wasabi, etc.
},
});
Create a GCS origin
import { Transcodely, OriginPermission } from "@transcodely/sdk";
const origin = await client.origins.create({
name: "Production GCS",
permissions: [OriginPermission.READ, OriginPermission.WRITE],
gcs: {
bucket: "my-gcs-bucket",
credentials: {
serviceAccountJson: process.env.GCS_SERVICE_ACCOUNT_JSON!,
},
},
});
Create an HTTP origin
import { Transcodely, OriginPermission } from "@transcodely/sdk";
const origin = await client.origins.create({
name: "Public CDN",
permissions: [OriginPermission.READ], // HTTP origins are read-only
http: {
baseUrl: "https://media.example.com",
credentials: {
headers: { Authorization: `Bearer ${process.env.MEDIA_TOKEN!}` },
},
},
});
Create an R2 origin
R2 supports two forms. With accountId (32-char hex) the endpoint is derived for you, optionally with a data-residency jurisdiction:
import { Transcodely, OriginPermission, R2Jurisdiction } from "@transcodely/sdk";
const origin = await client.origins.create({
name: "Production R2",
permissions: [OriginPermission.READ, OriginPermission.WRITE],
r2: {
bucket: "media",
accountId: process.env.R2_ACCOUNT_ID!,
jurisdiction: R2Jurisdiction.DEFAULT, // or .EU, .FEDRAMP
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY!,
secretAccessKey: process.env.R2_SECRET_KEY!,
},
},
});
Or, with an explicit endpoint (custom domain bound to a bucket, or a jurisdiction not yet enumerated):
r2: {
bucket: "media",
endpoint: "https://media.example.com",
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY!,
secretAccessKey: process.env.R2_SECRET_KEY!,
},
},
Provide either accountId or endpoint, never both. jurisdiction only applies when accountId is set.
Webhooks
Transcodely signs every webhook delivery with HMAC-SHA-256 using your endpoint's whsec_… secret. Verify the signature before trusting the body — client.webhooks.constructEvent validates the signature, parses the envelope, and returns a typed event:
import express from "express";
import { Transcodely, WebhookSignatureError, WebhookTimestampError } from "@transcodely/sdk";
const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
const app = express();
app.post(
"/webhooks/transcodely",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = client.webhooks.constructEvent(
req.body,
req.header("transcodely-signature")!,
process.env.WEBHOOK_SECRET!,
);
switch (event.type) {
case "job.succeeded":
// event.data is a fully-typed Job
console.log("Job done:", event.data.id);
break;
case "video.uploaded":
console.log("Video uploaded:", event.data.id);
break;
default:
// Forward-compat: unknown types still parse.
console.log("Unhandled:", event.type);
}
res.sendStatus(200);
} catch (err) {
if (err instanceof WebhookSignatureError || err instanceof WebhookTimestampError) {
res.sendStatus(400);
return;
}
throw err;
}
},
);
The signed payload is the raw HTTP body — use express.raw() (or the equivalent in your framework) to receive a Buffer, never express.json().
Multi-secret rotation
Pass an array to verify against both your previous and current secrets during a rotation window:
client.webhooks.constructEvent(body, sig, [process.env.PREVIOUS_SECRET!, process.env.CURRENT_SECRET!]);
Manage endpoints
const endpoint = await client.webhookEndpoints.create({
appId: "app_xyz",
url: "https://example.com/webhooks/transcodely",
enabledEvents: ["job.succeeded", "job.failed", "video.uploaded"],
});
console.log("Store this:", endpoint.secret); // only present on create + rotate
const rotated = await client.webhookEndpoints.rotateSecret(endpoint.id);
console.log("New secret:", rotated.secret);
for await (const ep of client.webhookEndpoints.list({ appId: "app_xyz" }).autoPage()) {
console.log(ep.id, ep.url);
}
await client.webhookEndpoints.sendTest(endpoint.id, "job.succeeded");
Replay an event
// Fetch a stored event (same shape as constructEvent returns)
const event = await client.events.retrieve("evt_…");
console.log(event.type, event.data);
// Requeue delivery — defaults to every subscribed endpoint, or pass
// `endpointIds` to target a subset.
await client.events.resend("evt_…");
Errors
All SDK errors extend TranscodelyError:
import { TranscodelyError, InvalidRequestError, RateLimitError } from "@transcodely/sdk";
try {
await client.jobs.create(params);
} catch (err) {
if (err instanceof InvalidRequestError) {
for (const v of err.errors) console.warn(`${v.field}: ${v.description}`);
} else if (err instanceof RateLimitError) {
await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 1000));
} else if (err instanceof TranscodelyError) {
console.error(err.code, err.message, err.requestId);
} else {
throw err;
}
}
The hierarchy:
| Class | Status | When |
|---|---|---|
APIConnectionError |
— | Network / DNS / TLS failure |
APIError |
5xx | Server-side error |
AuthenticationError |
401 | Bad / missing / revoked key |
PermissionError |
403 | Authenticated but forbidden |
NotFoundError |
404 | Resource doesn't exist |
ConflictError |
409 | Idempotency conflict, slug taken |
RateLimitError |
429 | Carries retryAfterMs |
InvalidRequestError |
400 | Carries errors (FieldViolation[]) |
PreconditionError |
412 | Wrong state (e.g. job not cancelable) |
Every error carries requestId, code, httpStatus, and raw for debugging.
Pagination
Every list method returns a Page you can either await for one page or auto-iterate:
// One page
const page = await client.jobs.list({ pagination: { limit: 50 } });
console.log(page.items, page.nextCursor);
// All items, automatically across pages
for await (const job of client.jobs.list({ pagination: { limit: 50 } }).autoPage()) {
console.log(job.id);
}
Idempotency
jobs.create accepts an idempotencyKey field. The SDK auto-generates a UUID if you don't pass one, so retries are always safe. For cross-process safety, pass your own:
await client.jobs.create({
inputUrl: "...",
outputs: [...],
idempotencyKey: "create-job-for-asset-12345",
});
For all other write methods, the SDK ships Idempotency-Key HTTP header automatically.
Streaming watch
const ac = new AbortController();
setTimeout(() => ac.abort(), 30_000); // give up after 30s
for await (const event of client.jobs.watch(job.id, { signal: ac.signal })) {
console.log(event.event, event.job?.status, event.job?.progress);
}
The SDK auto-reconnects on transient network failures (Watch is read-only, so resumption is idempotent — every reconnect emits a fresh SNAPSHOT event). Heartbeat events are filtered by default; pass includeHeartbeats: true to see them.
Configuration
new Transcodely({
apiKey: string, // required
baseUrl?: string, // default: https://api.transcodely.com
timeoutMs?: number, // unary-call timeout, default 30s
maxRetries?: number, // default 3
apiVersion?: string, // override the pinned API version
defaultHeaders?: Record<string, string>, // sent on every request
fetchImpl?: typeof fetch, // for browser DI / testing
logger?: (event: LogEvent) => void, // structured request logger
});
Request IDs
Each response carries X-Request-Id. Stripe-style:
console.log(client.lastRequestId); // "req_*"
try { await client.jobs.create(...); }
catch (err) {
if (err instanceof TranscodelyError) console.error("failed:", err.requestId);
}
Wire format
The SDK uses Connect-RPC over HTTP+JSON with snake_case field names and lowercase simplified enum values (e.g. "pending" instead of "JOB_STATUS_PENDING"). A custom codec handles the transformation transparently — the surface you write against is fully typed.
Versioning
The SDK is versioned independently with semver, starting at 0.1.0. Breaking changes are allowed on minor bumps until 1.0.0. Each release pins a specific calendar-versioned API (Transcodely.API_VERSION) and sends Transcodely-Version on every request.
License
MIT.