@transcodely/sdk 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Transcodely JavaScript / TypeScript SDK will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Breaking changes are allowed on minor bumps until 1.0.0.
6
+
7
+ ## [0.1.2](https://github.com/transcodely/transcodely-js/compare/v0.1.1...v0.1.2) (2026-07-01)
8
+
9
+
10
+ ### Features
11
+
12
+ * **origins:** add Cloudflare R2 as a first-class provider ([#4](https://github.com/transcodely/transcodely-js/issues/4)) ([6807153](https://github.com/transcodely/transcodely-js/commit/680715334e43a4b9968df52ab1a8d1c86fb0ac03))
13
+ * **webhooks:** add verify helper, typed events, and endpoint resources ([#6](https://github.com/transcodely/transcodely-js/issues/6)) ([97a4f3a](https://github.com/transcodely/transcodely-js/commit/97a4f3ade9ab0e7e90abf09eb414d03526e78d96))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **webhooks:** expand simplified enums when decoding event data ([#7](https://github.com/transcodely/transcodely-js/issues/7)) ([f05b487](https://github.com/transcodely/transcodely-js/commit/f05b487d72a77aa831e580591eebbc6b14e8af7b))
19
+
20
+ ## [0.1.1](https://github.com/transcodely/transcodely-js/compare/v0.1.0...v0.1.1) (2026-05-05)
21
+
22
+
23
+ ### Features
24
+
25
+ * initial 0.1.0 alpha release ([0f1d870](https://github.com/transcodely/transcodely-js/commit/0f1d87035d3dc510510bbdb5df1643089d6db6e0))
26
+
27
+ ## [0.1.0] — Alpha
28
+
29
+ Initial public alpha. Covers 100% of the public RPC surface (56 RPCs across 10 services). Stripe-style facade: lazy resource namespaces, auto-pagination, auto-idempotency on `create` mutations, typed error hierarchy (1 base + 8 concrete), Watch streams with auto-reconnect, calendar-versioned API.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Transcodely
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,363 @@
1
+ # @transcodely/sdk
2
+
3
+ Official TypeScript / Node SDK for the [Transcodely](https://transcodely.com) video transcoding API.
4
+
5
+ ```bash
6
+ npm install @transcodely/sdk
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```ts
12
+ import { Transcodely, OutputFormat, VideoCodec, Resolution } from "@transcodely/sdk";
13
+
14
+ const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
15
+
16
+ // Create a job
17
+ const job = await client.jobs.create({
18
+ inputUrl: "https://example.com/source.mp4",
19
+ outputs: [{
20
+ type: OutputFormat.HLS,
21
+ video: [
22
+ { codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_1080P },
23
+ { codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_720P },
24
+ ],
25
+ }],
26
+ });
27
+
28
+ console.log(job.id); // "job_a1b2c3d4e5f6"
29
+
30
+ // Watch progress in real time
31
+ for await (const event of client.jobs.watch(job.id)) {
32
+ console.log(event.job?.status, event.job?.progress);
33
+ if (event.job?.status === 4 /* COMPLETED */) break;
34
+ }
35
+ ```
36
+
37
+ ## Authentication
38
+
39
+ Pass your API key in the constructor:
40
+
41
+ ```ts
42
+ const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
43
+ ```
44
+
45
+ Test-mode keys (`ak_test_*`) and live-mode keys (`ak_live_*`) hit the same base URL — the environment is encoded in the key prefix.
46
+
47
+ ## Resources
48
+
49
+ ```ts
50
+ client.jobs // create / get / list / cancel / confirm / watch
51
+ client.videos // upload helpers, multipart, get / list / update / delete / watch
52
+ client.presets // create / get / getBySlug / list / update / duplicate / archive
53
+ client.origins // create / get / list / update / validate / archive
54
+ client.apps // create / get / list / update / archive / enableHosting
55
+ client.apiKeys // create / get / list / revoke
56
+ client.organizations // create / get / list / update / checkSlug
57
+ client.memberships // list / get / updateRole / remove
58
+ client.users // getMe / get / list / updateMe
59
+ client.health // check
60
+ ```
61
+
62
+ ## Origins
63
+
64
+ 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.
65
+
66
+ ### Create an S3 origin
67
+
68
+ ```ts
69
+ import { Transcodely, OriginPermission } from "@transcodely/sdk";
70
+
71
+ const origin = await client.origins.create({
72
+ name: "Production S3",
73
+ permissions: [OriginPermission.READ, OriginPermission.WRITE],
74
+ s3: {
75
+ bucket: "my-bucket",
76
+ region: "us-east-1",
77
+ credentials: {
78
+ accessKeyId: process.env.S3_ACCESS_KEY!,
79
+ secretAccessKey: process.env.S3_SECRET_KEY!,
80
+ },
81
+ // endpoint: "https://s3.custom.example.com", // for MinIO, Wasabi, etc.
82
+ },
83
+ });
84
+ ```
85
+
86
+ ### Create a GCS origin
87
+
88
+ ```ts
89
+ import { Transcodely, OriginPermission } from "@transcodely/sdk";
90
+
91
+ const origin = await client.origins.create({
92
+ name: "Production GCS",
93
+ permissions: [OriginPermission.READ, OriginPermission.WRITE],
94
+ gcs: {
95
+ bucket: "my-gcs-bucket",
96
+ credentials: {
97
+ serviceAccountJson: process.env.GCS_SERVICE_ACCOUNT_JSON!,
98
+ },
99
+ },
100
+ });
101
+ ```
102
+
103
+ ### Create an HTTP origin
104
+
105
+ ```ts
106
+ import { Transcodely, OriginPermission } from "@transcodely/sdk";
107
+
108
+ const origin = await client.origins.create({
109
+ name: "Public CDN",
110
+ permissions: [OriginPermission.READ], // HTTP origins are read-only
111
+ http: {
112
+ baseUrl: "https://media.example.com",
113
+ credentials: {
114
+ headers: { Authorization: `Bearer ${process.env.MEDIA_TOKEN!}` },
115
+ },
116
+ },
117
+ });
118
+ ```
119
+
120
+ ### Create an R2 origin
121
+
122
+ R2 supports two forms. With `accountId` (32-char hex) the endpoint is derived for you, optionally with a data-residency jurisdiction:
123
+
124
+ ```ts
125
+ import { Transcodely, OriginPermission, R2Jurisdiction } from "@transcodely/sdk";
126
+
127
+ const origin = await client.origins.create({
128
+ name: "Production R2",
129
+ permissions: [OriginPermission.READ, OriginPermission.WRITE],
130
+ r2: {
131
+ bucket: "media",
132
+ accountId: process.env.R2_ACCOUNT_ID!,
133
+ jurisdiction: R2Jurisdiction.DEFAULT, // or .EU, .FEDRAMP
134
+ credentials: {
135
+ accessKeyId: process.env.R2_ACCESS_KEY!,
136
+ secretAccessKey: process.env.R2_SECRET_KEY!,
137
+ },
138
+ },
139
+ });
140
+ ```
141
+
142
+ Or, with an explicit `endpoint` (custom domain bound to a bucket, or a jurisdiction not yet enumerated):
143
+
144
+ ```ts
145
+ r2: {
146
+ bucket: "media",
147
+ endpoint: "https://media.example.com",
148
+ credentials: {
149
+ accessKeyId: process.env.R2_ACCESS_KEY!,
150
+ secretAccessKey: process.env.R2_SECRET_KEY!,
151
+ },
152
+ },
153
+ ```
154
+
155
+ Provide either `accountId` or `endpoint`, never both. `jurisdiction` only applies when `accountId` is set.
156
+
157
+ ## Webhooks
158
+
159
+ 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:
160
+
161
+ ```ts
162
+ import express from "express";
163
+ import { Transcodely, WebhookSignatureError, WebhookTimestampError } from "@transcodely/sdk";
164
+
165
+ const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
166
+ const app = express();
167
+
168
+ app.post(
169
+ "/webhooks/transcodely",
170
+ express.raw({ type: "application/json" }),
171
+ (req, res) => {
172
+ try {
173
+ const event = client.webhooks.constructEvent(
174
+ req.body,
175
+ req.header("transcodely-signature")!,
176
+ process.env.WEBHOOK_SECRET!,
177
+ );
178
+
179
+ switch (event.type) {
180
+ case "job.succeeded":
181
+ // event.data is a fully-typed Job
182
+ console.log("Job done:", event.data.id);
183
+ break;
184
+ case "video.uploaded":
185
+ console.log("Video uploaded:", event.data.id);
186
+ break;
187
+ default:
188
+ // Forward-compat: unknown types still parse.
189
+ console.log("Unhandled:", event.type);
190
+ }
191
+ res.sendStatus(200);
192
+ } catch (err) {
193
+ if (err instanceof WebhookSignatureError || err instanceof WebhookTimestampError) {
194
+ res.sendStatus(400);
195
+ return;
196
+ }
197
+ throw err;
198
+ }
199
+ },
200
+ );
201
+ ```
202
+
203
+ The signed payload is the **raw HTTP body** — use `express.raw()` (or the equivalent in your framework) to receive a `Buffer`, never `express.json()`.
204
+
205
+ ### Multi-secret rotation
206
+
207
+ Pass an array to verify against both your previous and current secrets during a rotation window:
208
+
209
+ ```ts
210
+ client.webhooks.constructEvent(body, sig, [process.env.PREVIOUS_SECRET!, process.env.CURRENT_SECRET!]);
211
+ ```
212
+
213
+ ### Manage endpoints
214
+
215
+ ```ts
216
+ const endpoint = await client.webhookEndpoints.create({
217
+ appId: "app_xyz",
218
+ url: "https://example.com/webhooks/transcodely",
219
+ enabledEvents: ["job.succeeded", "job.failed", "video.uploaded"],
220
+ });
221
+ console.log("Store this:", endpoint.secret); // only present on create + rotate
222
+
223
+ const rotated = await client.webhookEndpoints.rotateSecret(endpoint.id);
224
+ console.log("New secret:", rotated.secret);
225
+
226
+ for await (const ep of client.webhookEndpoints.list({ appId: "app_xyz" }).autoPage()) {
227
+ console.log(ep.id, ep.url);
228
+ }
229
+
230
+ await client.webhookEndpoints.sendTest(endpoint.id, "job.succeeded");
231
+ ```
232
+
233
+ ### Replay an event
234
+
235
+ ```ts
236
+ // Fetch a stored event (same shape as constructEvent returns)
237
+ const event = await client.events.retrieve("evt_…");
238
+ console.log(event.type, event.data);
239
+
240
+ // Requeue delivery — defaults to every subscribed endpoint, or pass
241
+ // `endpointIds` to target a subset.
242
+ await client.events.resend("evt_…");
243
+ ```
244
+
245
+ ## Errors
246
+
247
+ All SDK errors extend `TranscodelyError`:
248
+
249
+ ```ts
250
+ import { TranscodelyError, InvalidRequestError, RateLimitError } from "@transcodely/sdk";
251
+
252
+ try {
253
+ await client.jobs.create(params);
254
+ } catch (err) {
255
+ if (err instanceof InvalidRequestError) {
256
+ for (const v of err.errors) console.warn(`${v.field}: ${v.description}`);
257
+ } else if (err instanceof RateLimitError) {
258
+ await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 1000));
259
+ } else if (err instanceof TranscodelyError) {
260
+ console.error(err.code, err.message, err.requestId);
261
+ } else {
262
+ throw err;
263
+ }
264
+ }
265
+ ```
266
+
267
+ The hierarchy:
268
+
269
+ | Class | Status | When |
270
+ |---|---|---|
271
+ | `APIConnectionError` | — | Network / DNS / TLS failure |
272
+ | `APIError` | 5xx | Server-side error |
273
+ | `AuthenticationError` | 401 | Bad / missing / revoked key |
274
+ | `PermissionError` | 403 | Authenticated but forbidden |
275
+ | `NotFoundError` | 404 | Resource doesn't exist |
276
+ | `ConflictError` | 409 | Idempotency conflict, slug taken |
277
+ | `RateLimitError` | 429 | Carries `retryAfterMs` |
278
+ | `InvalidRequestError` | 400 | Carries `errors` (`FieldViolation[]`) |
279
+ | `PreconditionError` | 412 | Wrong state (e.g. job not cancelable) |
280
+
281
+ Every error carries `requestId`, `code`, `httpStatus`, and `raw` for debugging.
282
+
283
+ ## Pagination
284
+
285
+ Every `list` method returns a `Page` you can either await for one page or auto-iterate:
286
+
287
+ ```ts
288
+ // One page
289
+ const page = await client.jobs.list({ pagination: { limit: 50 } });
290
+ console.log(page.items, page.nextCursor);
291
+
292
+ // All items, automatically across pages
293
+ for await (const job of client.jobs.list({ pagination: { limit: 50 } }).autoPage()) {
294
+ console.log(job.id);
295
+ }
296
+ ```
297
+
298
+ ## Idempotency
299
+
300
+ `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:
301
+
302
+ ```ts
303
+ await client.jobs.create({
304
+ inputUrl: "...",
305
+ outputs: [...],
306
+ idempotencyKey: "create-job-for-asset-12345",
307
+ });
308
+ ```
309
+
310
+ For all other write methods, the SDK ships `Idempotency-Key` HTTP header automatically.
311
+
312
+ ## Streaming watch
313
+
314
+ ```ts
315
+ const ac = new AbortController();
316
+ setTimeout(() => ac.abort(), 30_000); // give up after 30s
317
+
318
+ for await (const event of client.jobs.watch(job.id, { signal: ac.signal })) {
319
+ console.log(event.event, event.job?.status, event.job?.progress);
320
+ }
321
+ ```
322
+
323
+ 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.
324
+
325
+ ## Configuration
326
+
327
+ ```ts
328
+ new Transcodely({
329
+ apiKey: string, // required
330
+ baseUrl?: string, // default: https://api.transcodely.com
331
+ timeoutMs?: number, // unary-call timeout, default 30s
332
+ maxRetries?: number, // default 3
333
+ apiVersion?: string, // override the pinned API version
334
+ defaultHeaders?: Record<string, string>, // sent on every request
335
+ fetchImpl?: typeof fetch, // for browser DI / testing
336
+ logger?: (event: LogEvent) => void, // structured request logger
337
+ });
338
+ ```
339
+
340
+ ## Request IDs
341
+
342
+ Each response carries `X-Request-Id`. Stripe-style:
343
+
344
+ ```ts
345
+ console.log(client.lastRequestId); // "req_*"
346
+
347
+ try { await client.jobs.create(...); }
348
+ catch (err) {
349
+ if (err instanceof TranscodelyError) console.error("failed:", err.requestId);
350
+ }
351
+ ```
352
+
353
+ ## Wire format
354
+
355
+ 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.
356
+
357
+ ## Versioning
358
+
359
+ 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.
360
+
361
+ ## License
362
+
363
+ [MIT](LICENSE).