@convertrilo/sdk 0.0.4 → 0.0.6
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/README.md +17 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +9 -0
- package/dist/src/types.d.ts +73 -0
- package/docs/API-INTEGRATION-GUIDE.md +218 -0
- package/docs/WEBHOOKS.md +170 -0
- package/openapi.yaml +50 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@ The `examples/` directory contains starter scripts for the main integration path
|
|
|
37
37
|
Local SDK smoke tests use `.env`, but published SDK users should provide credentials through their
|
|
38
38
|
own server environment. Do not put Convertrilo API keys or customer storage tokens in frontend code.
|
|
39
39
|
|
|
40
|
+
For a complete server-to-server walkthrough covering URL, S3, folder ingest, Google Drive BYO
|
|
41
|
+
OAuth tokens, polling, and webhooks, see
|
|
42
|
+
[`docs/API-INTEGRATION-GUIDE.md`](docs/API-INTEGRATION-GUIDE.md).
|
|
43
|
+
|
|
40
44
|
## URL Source To CDN Output
|
|
41
45
|
|
|
42
46
|
```ts
|
|
@@ -163,6 +167,19 @@ for (const job of batch.jobs || []) {
|
|
|
163
167
|
|
|
164
168
|
Poll each returned `jobId` with `client.onDemandStatus(jobId)`.
|
|
165
169
|
|
|
170
|
+
## Webhook Delivery History
|
|
171
|
+
|
|
172
|
+
Managed webhooks are HMAC signed. You can test a webhook and inspect recent delivery attempts:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
await client.testWebhook(webhookId);
|
|
176
|
+
|
|
177
|
+
const history = await client.getWebhookDeliveries(webhookId);
|
|
178
|
+
for (const delivery of history.deliveries || []) {
|
|
179
|
+
console.log(delivery.status, delivery.statusCode, delivery.event);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
166
183
|
## Regenerate Types
|
|
167
184
|
|
|
168
185
|
The SDK types are generated from `openapi.yaml`.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -115,8 +115,22 @@ export declare class ConvertriloClient {
|
|
|
115
115
|
lastFailedAt?: string | null;
|
|
116
116
|
createdAt?: string;
|
|
117
117
|
}>;
|
|
118
|
+
updateWebhook(id: string, body: paths["/webhooks/{id}"]["patch"]["requestBody"]["content"]["application/json"]): Promise<{
|
|
119
|
+
id?: string;
|
|
120
|
+
url?: string;
|
|
121
|
+
events?: string[];
|
|
122
|
+
secret?: string;
|
|
123
|
+
isActive?: boolean;
|
|
124
|
+
failureCount?: number;
|
|
125
|
+
lastTriggeredAt?: string | null;
|
|
126
|
+
lastFailedAt?: string | null;
|
|
127
|
+
createdAt?: string;
|
|
128
|
+
}>;
|
|
118
129
|
deleteWebhook(id: string): Promise<unknown>;
|
|
119
130
|
testWebhook(id: string): Promise<unknown>;
|
|
131
|
+
getWebhookDeliveries(id: string): Promise<{
|
|
132
|
+
deliveries?: import("./types").components["schemas"]["WebhookDeliveryResponse"][];
|
|
133
|
+
}>;
|
|
120
134
|
initStream(id: string): Promise<{
|
|
121
135
|
ok?: boolean;
|
|
122
136
|
uploadId?: string;
|
package/dist/src/index.js
CHANGED
|
@@ -134,12 +134,21 @@ export class ConvertriloClient {
|
|
|
134
134
|
body: JSON.stringify(body),
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
+
async updateWebhook(id, body) {
|
|
138
|
+
return this.request(`/webhooks/${id}`, {
|
|
139
|
+
method: "PATCH",
|
|
140
|
+
body: JSON.stringify(body),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
137
143
|
async deleteWebhook(id) {
|
|
138
144
|
return this.request(`/webhooks/${id}`, { method: "DELETE" });
|
|
139
145
|
}
|
|
140
146
|
async testWebhook(id) {
|
|
141
147
|
return this.request(`/webhooks/${id}/test`, { method: "POST" });
|
|
142
148
|
}
|
|
149
|
+
async getWebhookDeliveries(id) {
|
|
150
|
+
return this.request(`/webhooks/${id}/deliveries`);
|
|
151
|
+
}
|
|
143
152
|
// Streaming
|
|
144
153
|
async initStream(id) {
|
|
145
154
|
return this.request(`/jobs/${id}/stream/init`, { method: "POST" });
|
package/dist/src/types.d.ts
CHANGED
|
@@ -707,6 +707,56 @@ export interface paths {
|
|
|
707
707
|
};
|
|
708
708
|
trace?: never;
|
|
709
709
|
};
|
|
710
|
+
"/webhooks/{id}/deliveries": {
|
|
711
|
+
parameters: {
|
|
712
|
+
query?: never;
|
|
713
|
+
header?: never;
|
|
714
|
+
path?: never;
|
|
715
|
+
cookie?: never;
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* List webhook delivery attempts
|
|
719
|
+
* @description Returns the 50 most recent managed or test delivery attempts for this webhook.
|
|
720
|
+
*/
|
|
721
|
+
get: {
|
|
722
|
+
parameters: {
|
|
723
|
+
query?: never;
|
|
724
|
+
header?: never;
|
|
725
|
+
path: {
|
|
726
|
+
id: string;
|
|
727
|
+
};
|
|
728
|
+
cookie?: never;
|
|
729
|
+
};
|
|
730
|
+
requestBody?: never;
|
|
731
|
+
responses: {
|
|
732
|
+
/** @description Recent delivery attempts */
|
|
733
|
+
200: {
|
|
734
|
+
headers: {
|
|
735
|
+
[name: string]: unknown;
|
|
736
|
+
};
|
|
737
|
+
content: {
|
|
738
|
+
"application/json": components["schemas"]["WebhookDeliveryListResponse"];
|
|
739
|
+
};
|
|
740
|
+
};
|
|
741
|
+
/** @description Webhook not found */
|
|
742
|
+
404: {
|
|
743
|
+
headers: {
|
|
744
|
+
[name: string]: unknown;
|
|
745
|
+
};
|
|
746
|
+
content: {
|
|
747
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
put?: never;
|
|
753
|
+
post?: never;
|
|
754
|
+
delete?: never;
|
|
755
|
+
options?: never;
|
|
756
|
+
head?: never;
|
|
757
|
+
patch?: never;
|
|
758
|
+
trace?: never;
|
|
759
|
+
};
|
|
710
760
|
"/jobs/bulk": {
|
|
711
761
|
parameters: {
|
|
712
762
|
query?: never;
|
|
@@ -1655,6 +1705,29 @@ export interface components {
|
|
|
1655
1705
|
WebhookListResponse: {
|
|
1656
1706
|
webhooks?: components["schemas"]["WebhookResponse"][];
|
|
1657
1707
|
};
|
|
1708
|
+
WebhookDeliveryResponse: {
|
|
1709
|
+
/** Format: uuid */
|
|
1710
|
+
id?: string;
|
|
1711
|
+
/** Format: uuid */
|
|
1712
|
+
webhookId?: string;
|
|
1713
|
+
event?: string;
|
|
1714
|
+
/** Format: uuid */
|
|
1715
|
+
jobId?: string | null;
|
|
1716
|
+
/** @enum {string} */
|
|
1717
|
+
status?: "success" | "failed";
|
|
1718
|
+
statusCode?: number | null;
|
|
1719
|
+
durationMs?: number | null;
|
|
1720
|
+
/** @description Response body captured from the receiver, truncated to 2048 characters. */
|
|
1721
|
+
responseBody?: string | null;
|
|
1722
|
+
/** @description Network, timeout, or delivery error, truncated to 2048 characters. */
|
|
1723
|
+
error?: string | null;
|
|
1724
|
+
attempt?: number;
|
|
1725
|
+
/** Format: date-time */
|
|
1726
|
+
createdAt?: string;
|
|
1727
|
+
};
|
|
1728
|
+
WebhookDeliveryListResponse: {
|
|
1729
|
+
deliveries?: components["schemas"]["WebhookDeliveryResponse"][];
|
|
1730
|
+
};
|
|
1658
1731
|
S3Output: {
|
|
1659
1732
|
bucket: string;
|
|
1660
1733
|
key: string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# API Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide is for server-to-server integrations that want to use Convertrilo as a low-cost video encoding backend.
|
|
4
|
+
|
|
5
|
+
Do not call Convertrilo directly from browser or mobile apps. Keep Convertrilo API keys, S3 credentials, and Google OAuth tokens on your backend.
|
|
6
|
+
|
|
7
|
+
## Core Model
|
|
8
|
+
|
|
9
|
+
1. Your app authenticates your user.
|
|
10
|
+
2. Your backend collects or owns the source video location.
|
|
11
|
+
3. Your backend calls Convertrilo with an API key.
|
|
12
|
+
4. Convertrilo queues one or more encode jobs.
|
|
13
|
+
5. Your backend tracks completion by polling job status or receiving managed webhooks.
|
|
14
|
+
|
|
15
|
+
API users do not need to connect Google Drive in the Convertrilo dashboard. For Google Drive integrations, your app should run its own Google OAuth flow and pass customer-owned `accessToken` and optional `refreshToken` values to Convertrilo.
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
18
|
+
|
|
19
|
+
Create an API key in the dashboard Developer page and send it with every request:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl https://api.convertrilo.com/tokens/balance \
|
|
23
|
+
-H "X-API-Key: $CONVERTRILO_API_KEY"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use API keys with the minimum scopes needed by your integration. Most encode integrations need:
|
|
27
|
+
|
|
28
|
+
- `jobs:create`
|
|
29
|
+
- `jobs:read`
|
|
30
|
+
- `jobs:cancel` if you expose cancellation
|
|
31
|
+
|
|
32
|
+
## TypeScript SDK
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add @convertrilo/sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { ConvertriloClient } from "@convertrilo/sdk";
|
|
40
|
+
|
|
41
|
+
const client = new ConvertriloClient({
|
|
42
|
+
baseUrl: "https://api.convertrilo.com",
|
|
43
|
+
apiKey: process.env.CONVERTRILO_API_KEY,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
SDK source and examples: https://github.com/serkandrgn/convertrilo-js
|
|
48
|
+
|
|
49
|
+
## Flow 1: URL Source To CDN Output
|
|
50
|
+
|
|
51
|
+
Use this when the source video is already available over HTTP(S), and you want Convertrilo to return a signed CDN download URL.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const job = await client.onDemandEncode({
|
|
55
|
+
sourceUrl: "https://example.com/input.mp4",
|
|
56
|
+
codec: "h264",
|
|
57
|
+
resolution: "1080p",
|
|
58
|
+
quality: "better",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(job.jobId);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Equivalent curl:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
curl https://api.convertrilo.com/ondemand/encode \
|
|
68
|
+
-H "Content-Type: application/json" \
|
|
69
|
+
-H "X-API-Key: $CONVERTRILO_API_KEY" \
|
|
70
|
+
-d '{
|
|
71
|
+
"sourceUrl": "https://example.com/input.mp4",
|
|
72
|
+
"codec": "h264",
|
|
73
|
+
"resolution": "1080p",
|
|
74
|
+
"quality": "better"
|
|
75
|
+
}'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Poll `/ondemand/status/{jobId}` until the job reaches `success`, then read `downloadUrl`.
|
|
79
|
+
|
|
80
|
+
## Flow 2: URL Source To S3 Output
|
|
81
|
+
|
|
82
|
+
Use this when your customer wants the encoded output in their own S3-compatible bucket.
|
|
83
|
+
|
|
84
|
+
The output credentials need permission to write the final object.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const job = await client.onDemandEncode({
|
|
88
|
+
sourceUrl: "https://example.com/input.mp4",
|
|
89
|
+
codec: "h264",
|
|
90
|
+
resolution: "1080p",
|
|
91
|
+
outputS3: {
|
|
92
|
+
bucket: "customer-output-bucket",
|
|
93
|
+
key: "encoded/input-1080p.mp4",
|
|
94
|
+
region: "us-east-1",
|
|
95
|
+
endpoint: process.env.CUSTOMER_S3_ENDPOINT,
|
|
96
|
+
accessKeyId: process.env.CUSTOMER_S3_ACCESS_KEY_ID,
|
|
97
|
+
secretAccessKey: process.env.CUSTOMER_S3_SECRET_ACCESS_KEY,
|
|
98
|
+
forcePathStyle: true,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For AWS S3, `endpoint` is usually unnecessary. For S3-compatible providers such as Cloudflare R2, MinIO, or object storage providers, pass `endpoint` and usually `forcePathStyle: true`.
|
|
104
|
+
|
|
105
|
+
## Flow 3: S3 Folder To S3 Output
|
|
106
|
+
|
|
107
|
+
Use this for batch compression. Convertrilo lists a source prefix, filters video files, and queues one encode job per video.
|
|
108
|
+
|
|
109
|
+
Source credentials need permission to list the prefix and read objects. Output credentials need permission to write encoded objects.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const batch = await client.onDemandIngestFolder({
|
|
113
|
+
sourceS3: {
|
|
114
|
+
bucket: "customer-source-bucket",
|
|
115
|
+
prefix: "incoming/",
|
|
116
|
+
region: "us-east-1",
|
|
117
|
+
endpoint: process.env.CUSTOMER_S3_ENDPOINT,
|
|
118
|
+
accessKeyId: process.env.CUSTOMER_S3_ACCESS_KEY_ID,
|
|
119
|
+
secretAccessKey: process.env.CUSTOMER_S3_SECRET_ACCESS_KEY,
|
|
120
|
+
forcePathStyle: true,
|
|
121
|
+
},
|
|
122
|
+
outputDestination: "s3",
|
|
123
|
+
outputS3: {
|
|
124
|
+
bucket: "customer-output-bucket",
|
|
125
|
+
prefix: "encoded/",
|
|
126
|
+
region: "us-east-1",
|
|
127
|
+
endpoint: process.env.CUSTOMER_S3_ENDPOINT,
|
|
128
|
+
accessKeyId: process.env.CUSTOMER_S3_ACCESS_KEY_ID,
|
|
129
|
+
secretAccessKey: process.env.CUSTOMER_S3_SECRET_ACCESS_KEY,
|
|
130
|
+
forcePathStyle: true,
|
|
131
|
+
},
|
|
132
|
+
codec: "h264",
|
|
133
|
+
resolution: "1080p",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
for (const job of batch.jobs || []) {
|
|
137
|
+
console.log(job.jobId, job.fileName);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Only files with video extensions are queued. If no video files are found, the API returns `404`.
|
|
142
|
+
|
|
143
|
+
## Flow 4: Google Drive With BYO OAuth Tokens
|
|
144
|
+
|
|
145
|
+
Use this when your app already owns the customer relationship and can run Google OAuth itself.
|
|
146
|
+
|
|
147
|
+
Do not send customers to the Convertrilo dashboard OAuth flow for API usage. Your app should request the Google scopes it needs, store tokens on your backend, and pass those tokens to Convertrilo per request.
|
|
148
|
+
|
|
149
|
+
For output-only jobs:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const job = await client.onDemandEncode({
|
|
153
|
+
sourceUrl: "https://example.com/input.mp4",
|
|
154
|
+
codec: "h264",
|
|
155
|
+
resolution: "1080p",
|
|
156
|
+
outputGoogleDrive: {
|
|
157
|
+
folderId: "GOOGLE_DRIVE_OUTPUT_FOLDER_ID",
|
|
158
|
+
fileName: "input-1080p.mp4",
|
|
159
|
+
accessToken: customerGoogleAccessToken,
|
|
160
|
+
refreshToken: customerGoogleRefreshToken,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For folder ingest from Google Drive to Google Drive:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const batch = await client.onDemandIngestFolder({
|
|
169
|
+
sourceGoogleDrive: {
|
|
170
|
+
folderId: "SOURCE_FOLDER_ID",
|
|
171
|
+
accessToken: customerGoogleAccessToken,
|
|
172
|
+
refreshToken: customerGoogleRefreshToken,
|
|
173
|
+
},
|
|
174
|
+
outputDestination: "google-drive",
|
|
175
|
+
outputGoogleDrive: {
|
|
176
|
+
folderId: "OUTPUT_FOLDER_ID",
|
|
177
|
+
accessToken: customerGoogleAccessToken,
|
|
178
|
+
refreshToken: customerGoogleRefreshToken,
|
|
179
|
+
},
|
|
180
|
+
codec: "h264",
|
|
181
|
+
resolution: "1080p",
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Include `refreshToken` when jobs may outlive a short-lived access token. Without a valid access token or refresh token, Google Drive folder ingest returns `401`.
|
|
186
|
+
|
|
187
|
+
## Tracking Completion
|
|
188
|
+
|
|
189
|
+
For simple integrations, poll status:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
async function waitForOnDemandJob(jobId: string) {
|
|
193
|
+
while (true) {
|
|
194
|
+
const status = await client.onDemandStatus(jobId);
|
|
195
|
+
|
|
196
|
+
if (status.status === "success") return status;
|
|
197
|
+
if (status.status === "failed" || status.status === "canceled") {
|
|
198
|
+
throw new Error(status.failureMessage || `Job ${status.status}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For production workflows, prefer managed webhooks. Managed webhooks are HMAC signed and are better for async pipelines. See [WEBHOOKS.md](WEBHOOKS.md).
|
|
207
|
+
|
|
208
|
+
## Error Handling
|
|
209
|
+
|
|
210
|
+
Common responses:
|
|
211
|
+
|
|
212
|
+
- `400`: invalid request payload
|
|
213
|
+
- `401`: missing API auth or missing/expired Google Drive token
|
|
214
|
+
- `403`: API key does not have the required scope
|
|
215
|
+
- `404`: folder ingest found no video files
|
|
216
|
+
- `410`: Dropbox source or destination was requested; Dropbox is deprecated
|
|
217
|
+
|
|
218
|
+
Treat encode job failure separately from request failure. A request can return `200` because the job was queued, then the job can later fail during download, encode, upload, or token refresh.
|
package/docs/WEBHOOKS.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Webhooks
|
|
2
|
+
|
|
3
|
+
Convertrilo supports managed webhook subscriptions for job lifecycle events.
|
|
4
|
+
|
|
5
|
+
Create managed webhooks in the dashboard Developer page or with `POST /webhooks`.
|
|
6
|
+
Managed webhook deliveries are signed with HMAC-SHA256.
|
|
7
|
+
|
|
8
|
+
## Events
|
|
9
|
+
|
|
10
|
+
- `job.created`
|
|
11
|
+
- `job.queued`
|
|
12
|
+
- `job.running`
|
|
13
|
+
- `job.completed`
|
|
14
|
+
- `job.failed`
|
|
15
|
+
- `job.canceled`
|
|
16
|
+
- `webhook.test` for manual test deliveries
|
|
17
|
+
|
|
18
|
+
## Headers
|
|
19
|
+
|
|
20
|
+
Managed webhook deliveries include:
|
|
21
|
+
|
|
22
|
+
```txt
|
|
23
|
+
Content-Type: application/json
|
|
24
|
+
X-Webhook-Signature: <hex hmac sha256>
|
|
25
|
+
X-Webhook-Event: job.completed
|
|
26
|
+
X-Webhook-Id: <webhook id>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The signature is:
|
|
30
|
+
|
|
31
|
+
```txt
|
|
32
|
+
hex(hmac_sha256(raw_request_body, webhook_secret))
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use the raw request body exactly as received. Do not parse and stringify JSON before verifying.
|
|
36
|
+
|
|
37
|
+
## Payload
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"event": "job.completed",
|
|
42
|
+
"timestamp": "2026-06-05T00:00:00.000Z",
|
|
43
|
+
"data": {
|
|
44
|
+
"jobId": "550e8400-e29b-41d4-a716-446655440000",
|
|
45
|
+
"userId": "9c38f5dd-d9d6-4d08-a514-41e166dfbb8b",
|
|
46
|
+
"status": "success",
|
|
47
|
+
"encoder": "h264_nvenc",
|
|
48
|
+
"durationSec": 42,
|
|
49
|
+
"finalNeu": 2.5
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Failure events include `error` and `failureCode` when available.
|
|
55
|
+
|
|
56
|
+
## Verify In Node / Express
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import crypto from "node:crypto";
|
|
60
|
+
import express from "express";
|
|
61
|
+
|
|
62
|
+
const app = express();
|
|
63
|
+
const webhookSecret = process.env.CONVERTRILO_WEBHOOK_SECRET!;
|
|
64
|
+
|
|
65
|
+
app.post(
|
|
66
|
+
"/webhooks/convertrilo",
|
|
67
|
+
express.raw({ type: "application/json" }),
|
|
68
|
+
(req, res) => {
|
|
69
|
+
const signature = req.header("X-Webhook-Signature") || "";
|
|
70
|
+
const expected = crypto
|
|
71
|
+
.createHmac("sha256", webhookSecret)
|
|
72
|
+
.update(req.body)
|
|
73
|
+
.digest("hex");
|
|
74
|
+
|
|
75
|
+
const valid =
|
|
76
|
+
signature.length === expected.length &&
|
|
77
|
+
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
78
|
+
|
|
79
|
+
if (!valid) {
|
|
80
|
+
return res.status(401).send("Invalid signature");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const payload = JSON.parse(req.body.toString("utf8"));
|
|
84
|
+
console.log(payload.event, payload.data.jobId);
|
|
85
|
+
|
|
86
|
+
return res.sendStatus(204);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Verify In Next.js Route Handler
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import crypto from "node:crypto";
|
|
95
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
96
|
+
|
|
97
|
+
export async function POST(req: NextRequest) {
|
|
98
|
+
const rawBody = await req.text();
|
|
99
|
+
const signature = req.headers.get("x-webhook-signature") || "";
|
|
100
|
+
const secret = process.env.CONVERTRILO_WEBHOOK_SECRET!;
|
|
101
|
+
|
|
102
|
+
const expected = crypto
|
|
103
|
+
.createHmac("sha256", secret)
|
|
104
|
+
.update(rawBody)
|
|
105
|
+
.digest("hex");
|
|
106
|
+
|
|
107
|
+
const valid =
|
|
108
|
+
signature.length === expected.length &&
|
|
109
|
+
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
110
|
+
|
|
111
|
+
if (!valid) {
|
|
112
|
+
return new NextResponse("Invalid signature", { status: 401 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const payload = JSON.parse(rawBody);
|
|
116
|
+
console.log(payload.event, payload.data.jobId);
|
|
117
|
+
|
|
118
|
+
return new NextResponse(null, { status: 204 });
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Delivery Behavior
|
|
123
|
+
|
|
124
|
+
- Timeout: 15 seconds
|
|
125
|
+
- Success: any `2xx` response
|
|
126
|
+
- Failure: non-`2xx`, network error, or timeout
|
|
127
|
+
- A webhook is automatically disabled after 10 consecutive failures
|
|
128
|
+
- Re-enable it with `PATCH /webhooks/{id}` and `{ "isActive": true }`
|
|
129
|
+
|
|
130
|
+
Current deliveries are best-effort. There is not yet a durable retry queue.
|
|
131
|
+
|
|
132
|
+
## Delivery History
|
|
133
|
+
|
|
134
|
+
Recent managed and test delivery attempts are available with:
|
|
135
|
+
|
|
136
|
+
```txt
|
|
137
|
+
GET /webhooks/{id}/deliveries
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The response includes the 50 most recent attempts for that webhook:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"deliveries": [
|
|
145
|
+
{
|
|
146
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
147
|
+
"webhookId": "4a5c26a0-7b04-4d37-8bb1-446655440000",
|
|
148
|
+
"event": "job.completed",
|
|
149
|
+
"jobId": "9db109f6-6a88-49d7-89e2-446655440000",
|
|
150
|
+
"status": "success",
|
|
151
|
+
"statusCode": 204,
|
|
152
|
+
"durationMs": 182,
|
|
153
|
+
"responseBody": null,
|
|
154
|
+
"error": null,
|
|
155
|
+
"attempt": 1,
|
|
156
|
+
"createdAt": "2026-06-09T07:30:00.000Z"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`responseBody` and `error` are capped at 2048 characters.
|
|
163
|
+
|
|
164
|
+
## One-Off On-Demand Webhook URL
|
|
165
|
+
|
|
166
|
+
`POST /ondemand/encode` also accepts a `webhook` URL. That URL receives one terminal callback for
|
|
167
|
+
`job.completed`, `job.failed`, or `job.canceled`.
|
|
168
|
+
|
|
169
|
+
This one-off URL is best-effort and unsigned because it has no stored secret. For production
|
|
170
|
+
workflow integrations, prefer managed webhooks.
|
package/openapi.yaml
CHANGED
|
@@ -345,6 +345,33 @@ components:
|
|
|
345
345
|
type: array
|
|
346
346
|
items:
|
|
347
347
|
$ref: "#/components/schemas/WebhookResponse"
|
|
348
|
+
WebhookDeliveryResponse:
|
|
349
|
+
type: object
|
|
350
|
+
properties:
|
|
351
|
+
id: { type: string, format: uuid }
|
|
352
|
+
webhookId: { type: string, format: uuid }
|
|
353
|
+
event: { type: string }
|
|
354
|
+
jobId: { type: string, format: uuid, nullable: true }
|
|
355
|
+
status: { type: string, enum: [success, failed] }
|
|
356
|
+
statusCode: { type: integer, nullable: true }
|
|
357
|
+
durationMs: { type: integer, nullable: true }
|
|
358
|
+
responseBody:
|
|
359
|
+
type: string
|
|
360
|
+
nullable: true
|
|
361
|
+
description: Response body captured from the receiver, truncated to 2048 characters.
|
|
362
|
+
error:
|
|
363
|
+
type: string
|
|
364
|
+
nullable: true
|
|
365
|
+
description: Network, timeout, or delivery error, truncated to 2048 characters.
|
|
366
|
+
attempt: { type: integer }
|
|
367
|
+
createdAt: { type: string, format: date-time }
|
|
368
|
+
WebhookDeliveryListResponse:
|
|
369
|
+
type: object
|
|
370
|
+
properties:
|
|
371
|
+
deliveries:
|
|
372
|
+
type: array
|
|
373
|
+
items:
|
|
374
|
+
$ref: "#/components/schemas/WebhookDeliveryResponse"
|
|
348
375
|
|
|
349
376
|
# On-Demand Encoding
|
|
350
377
|
S3Output:
|
|
@@ -897,6 +924,29 @@ paths:
|
|
|
897
924
|
schema: { type: string, format: uuid }
|
|
898
925
|
responses:
|
|
899
926
|
"200": { description: Test event sent }
|
|
927
|
+
/webhooks/{id}/deliveries:
|
|
928
|
+
get:
|
|
929
|
+
security: [{ BearerAuth: [] }]
|
|
930
|
+
summary: List webhook delivery attempts
|
|
931
|
+
description: Returns the 50 most recent managed or test delivery attempts for this webhook.
|
|
932
|
+
parameters:
|
|
933
|
+
- in: path
|
|
934
|
+
name: id
|
|
935
|
+
required: true
|
|
936
|
+
schema: { type: string, format: uuid }
|
|
937
|
+
responses:
|
|
938
|
+
"200":
|
|
939
|
+
description: Recent delivery attempts
|
|
940
|
+
content:
|
|
941
|
+
application/json:
|
|
942
|
+
schema:
|
|
943
|
+
$ref: "#/components/schemas/WebhookDeliveryListResponse"
|
|
944
|
+
"404":
|
|
945
|
+
description: Webhook not found
|
|
946
|
+
content:
|
|
947
|
+
application/json:
|
|
948
|
+
schema:
|
|
949
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
900
950
|
|
|
901
951
|
/jobs/bulk:
|
|
902
952
|
post:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convertrilo/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "TypeScript client for the Convertrilo video encoding API",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"dist/src",
|
|
37
37
|
"examples",
|
|
38
|
+
"docs",
|
|
38
39
|
"openapi.yaml",
|
|
39
40
|
"README.md",
|
|
40
41
|
"LICENSE"
|