Overview
By default, transcoding jobs start encoding immediately after the input file is probed. The delayed start workflow adds a confirmation step: the job pauses after probing, gives you the estimated cost, and waits for explicit confirmation before proceeding.
This is useful when you need to:
- Show end users the transcoding cost before charging them
- Implement approval workflows for expensive jobs
- Validate estimated costs against a budget before committing
- Let users review input metadata (duration, resolution, codecs) before encoding
How It Works
The delayed job workflow has three phases:
Create (delayed_start: true)
|
v
Pending --> Probing --> Awaiting Confirmation
|
[Review estimated cost]
|
+------------+------------+
| |
Confirm Cancel
| |
v v
Processing Canceled
|
v
Completed- Create — Submit the job with
delayed_start: true. Status ispending. - Probe — Transcodely analyzes the input file to determine duration, resolution, and codec information. Status moves to
probing, thenawaiting_confirmation. - Confirm or Cancel — Review the estimated cost and either confirm the job to start encoding, or cancel it to avoid charges.
Step 1: Create a Delayed Job
Set delayed_start to true in your create request:
curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Create
-H "Content-Type: application/json"
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-d '{
"input_origin_id": "ori_input12345",
"input_path": "uploads/my-video.mp4",
"output_origin_id": "ori_output6789",
"delayed_start": true,
"outputs": [
{
"type": "mp4",
"video": [{ "codec": "h264", "resolution": "1080p", "quality": "standard" }]
},
{
"type": "hls",
"video": [
{ "codec": "h264", "resolution": "1080p", "quality": "standard" },
{ "codec": "h264", "resolution": "720p", "quality": "standard" },
{ "codec": "h264", "resolution": "480p", "quality": "economy" }
]
}
]
}'const job = await client.jobs.create({
inputOriginId: "ori_input12345",
inputPath: "uploads/my-video.mp4",
outputOriginId: "ori_output6789",
delayedStart: true,
outputs: [
{
type: OutputFormat.MP4,
video: [
{
codec: VideoCodec.H264,
resolution: Resolution.RESOLUTION_1080P,
quality: QualityTier.STANDARD,
},
],
},
{
type: OutputFormat.HLS,
video: [
{ codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_1080P, quality: QualityTier.STANDARD },
{ codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_720P, quality: QualityTier.STANDARD },
{ codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_480P, quality: QualityTier.ECONOMY },
],
},
],
});job = client.jobs.create(
input_origin_id="ori_input12345",
input_path="uploads/my-video.mp4",
output_origin_id="ori_output6789",
delayed_start=True,
outputs=[
{
"type": "mp4",
"video": [{"codec": "h264", "resolution": "1080p", "quality": "standard"}],
},
{
"type": "hls",
"video": [
{"codec": "h264", "resolution": "1080p", "quality": "standard"},
{"codec": "h264", "resolution": "720p", "quality": "standard"},
{"codec": "h264", "resolution": "480p", "quality": "economy"},
],
},
],
)job, err := client.Jobs.Create(ctx, &transcodely.JobCreateParams{
InputOriginId: proto.String("ori_input12345"),
InputPath: proto.String("uploads/my-video.mp4"),
OutputOriginId: proto.String("ori_output6789"),
DelayedStart: true,
Outputs: []*transcodely.OutputSpec{
{
Type: transcodely.OutputFormatMP4,
Video: []*transcodely.VideoVariant{{
Codec: transcodely.VideoCodecH264,
Resolution: transcodely.Resolution1080P,
Quality: transcodely.QualityTierStandard,
}},
},
{
Type: transcodely.OutputFormatHLS,
Video: []*transcodely.VideoVariant{
{Codec: transcodely.VideoCodecH264, Resolution: transcodely.Resolution1080P, Quality: transcodely.QualityTierStandard},
{Codec: transcodely.VideoCodecH264, Resolution: transcodely.Resolution720P, Quality: transcodely.QualityTierStandard},
{Codec: transcodely.VideoCodecH264, Resolution: transcodely.Resolution480P, Quality: transcodely.QualityTierEconomy},
},
},
},
})The response confirms the job was created with delayed_start enabled:
{
"job": {
"id": "job_a1b2c3d4e5f6",
"status": "pending",
"delayed_start": true,
"outputs": [
{ "id": "out_mp4_001", "status": "pending", "progress": 0 },
{ "id": "out_hls_002", "status": "pending", "progress": 0 }
],
"metadata": {},
"created_at": "2026-02-28T10:30:00Z"
}
}Step 2: Wait for Probing to Complete
The job automatically transitions through probing to awaiting_confirmation. You can poll the job status or use webhooks to be notified.
Polling
curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Get
-H "Content-Type: application/json"
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-d '{ "id": "job_a1b2c3d4e5f6" }'const job = await client.jobs.get("job_a1b2c3d4e5f6");job = client.jobs.get(id="job_a1b2c3d4e5f6")job, err := client.Jobs.Get(ctx, "job_a1b2c3d4e5f6")When the status is awaiting_confirmation, the response includes estimated costs and input metadata:
{
"job": {
"id": "job_a1b2c3d4e5f6",
"status": "awaiting_confirmation",
"delayed_start": true,
"total_estimated_cost": 0.3455,
"currency": "EUR",
"input_metadata": {
"duration_seconds": 120,
"width": 1920,
"height": 1080,
"framerate": 30.0,
"video_codec": "h264",
"audio_codec": "aac",
"file_size_bytes": 52428800
},
"outputs": [
{
"id": "out_mp4_001",
"status": "pending",
"estimated_cost": 0.12
},
{
"id": "out_hls_002",
"status": "pending",
"estimated_cost": 0.2255,
"variant_pricing": [
{ "index": 0, "resolution": "1080p", "estimated_cost": 0.106 },
{ "index": 1, "resolution": "720p", "estimated_cost": 0.0795 },
{ "index": 2, "resolution": "480p", "estimated_cost": 0.04 }
]
}
],
"probed_at": "2026-02-28T10:30:15Z"
}
}Using Watch for Real-Time Updates
Instead of polling, you can use the Watch RPC to stream status updates:
import { JobStatus } from "transcodely";
for await (const event of client.jobs.watch("job_a1b2c3d4e5f6")) {
if (event.job?.status === JobStatus.AWAITING_CONFIRMATION) {
console.warn("Estimated cost:", event.job.totalEstimatedCost, event.job.currency);
// Show cost to user, prompt for confirmation
break;
}
}from transcodely.v1 import job_pb2
for event in client.jobs.watch(id="job_a1b2c3d4e5f6"):
if event.job.status == job_pb2.JOB_STATUS_AWAITING_CONFIRMATION:
print("Estimated cost:", event.job.total_estimated_cost, event.job.currency)
# Show cost to user, prompt for confirmation
breakstream := client.Jobs.Watch(ctx, "job_a1b2c3d4e5f6")
defer stream.Close()
for stream.Next() {
event := stream.Current()
if event.GetJob().GetStatus() == transcodely.JobStatusAwaitingConfirmation {
job := event.GetJob()
fmt.Printf("Estimated cost: %.4f %s\n", job.GetTotalEstimatedCost(), job.GetCurrency())
// Show cost to user, prompt for confirmation
break
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}Step 3: Review the Estimate
Before confirming, review the cost breakdown:
| Field | Description |
|---|---|
total_estimated_cost | Sum of all output estimated costs |
currency | ISO 4217 currency code (e.g., EUR) |
outputs[].estimated_cost | Per-output cost |
outputs[].variant_pricing[] | Per-variant cost breakdown (for ABR outputs) |
input_metadata | Source file properties (duration, resolution, codecs) |
Estimated costs are calculated using locked pricing snapshots captured at job creation time. This means the price will not change between creation and confirmation, even if Transcodely updates its pricing in the interim.
Step 4: Confirm or Cancel
Confirm the Job
To proceed with encoding:
curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Confirm
-H "Content-Type: application/json"
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-d '{ "id": "job_a1b2c3d4e5f6" }'const job = await client.jobs.confirm("job_a1b2c3d4e5f6");job = client.jobs.confirm(id="job_a1b2c3d4e5f6")job, err := client.Jobs.Confirm(ctx, "job_a1b2c3d4e5f6")The job status transitions to processing and encoding begins:
{
"job": {
"id": "job_a1b2c3d4e5f6",
"status": "processing",
"delayed_start": true,
"confirmed_at": "2026-02-28T10:31:00Z"
}
}Cancel the Job
If the estimated cost is too high or the input is not what was expected, cancel the job:
curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Cancel
-H "Content-Type: application/json"
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-d '{ "id": "job_a1b2c3d4e5f6" }'const job = await client.jobs.cancel("job_a1b2c3d4e5f6");job = client.jobs.cancel(id="job_a1b2c3d4e5f6")job, err := client.Jobs.Cancel(ctx, "job_a1b2c3d4e5f6")Canceled jobs incur no encoding charges. The only cost is the brief probing phase.
Implementation Example
Here is a typical integration pattern for delayed jobs in a web application:
import {
Transcodely,
JobStatus,
OutputFormat,
VideoCodec,
Resolution,
QualityTier,
} from "transcodely";
const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
// 1. Create the delayed job
let job = await client.jobs.create({
inputOriginId: "ori_input12345",
inputPath: "uploads/my-video.mp4",
outputOriginId: "ori_output6789",
delayedStart: true,
outputs: [
{
type: OutputFormat.MP4,
video: [
{
codec: VideoCodec.H264,
resolution: Resolution.RESOLUTION_1080P,
quality: QualityTier.STANDARD,
},
],
},
],
});
// 2. Poll until awaiting confirmation
while (job.status !== JobStatus.AWAITING_CONFIRMATION) {
await new Promise((resolve) => setTimeout(resolve, 2000));
job = await client.jobs.get(job.id);
}
// 3. Show cost to user and get approval
const approved = await showCostApprovalDialog(job.totalEstimatedCost, job.currency);
// 4. Confirm or cancel
if (approved) {
await client.jobs.confirm(job.id);
} else {
await client.jobs.cancel(job.id);
}import os
import time
from transcodely import Transcodely
from transcodely.v1 import job_pb2
with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
# 1. Create the delayed job
job = client.jobs.create(
input_origin_id="ori_input12345",
input_path="uploads/my-video.mp4",
output_origin_id="ori_output6789",
delayed_start=True,
outputs=[
{
"type": "mp4",
"video": [{"codec": "h264", "resolution": "1080p", "quality": "standard"}],
},
],
)
# 2. Poll until awaiting confirmation
while job.status != job_pb2.JOB_STATUS_AWAITING_CONFIRMATION:
time.sleep(2)
job = client.jobs.get(id=job.id)
# 3. Show cost to user and get approval
approved = show_cost_approval_dialog(job.total_estimated_cost, job.currency)
# 4. Confirm or cancel
if approved:
client.jobs.confirm(id=job.id)
else:
client.jobs.cancel(id=job.id)package main
import (
"context"
"log"
"os"
"time"
"github.com/transcodely/transcodely-go"
"google.golang.org/protobuf/proto"
)
func main() {
ctx := context.Background()
client, err := transcodely.New(os.Getenv("TRANSCODELY_API_KEY"))
if err != nil {
log.Fatal(err)
}
// 1. Create the delayed job
job, err := client.Jobs.Create(ctx, &transcodely.JobCreateParams{
InputOriginId: proto.String("ori_input12345"),
InputPath: proto.String("uploads/my-video.mp4"),
OutputOriginId: proto.String("ori_output6789"),
DelayedStart: true,
Outputs: []*transcodely.OutputSpec{{
Type: transcodely.OutputFormatMP4,
Video: []*transcodely.VideoVariant{{
Codec: transcodely.VideoCodecH264,
Resolution: transcodely.Resolution1080P,
Quality: transcodely.QualityTierStandard,
}},
}},
})
if err != nil {
log.Fatal(err)
}
// 2. Poll until awaiting confirmation
for job.GetStatus() != transcodely.JobStatusAwaitingConfirmation {
time.Sleep(2 * time.Second)
job, err = client.Jobs.Get(ctx, job.GetId())
if err != nil {
log.Fatal(err)
}
}
// 3. Show cost to user and get approval
approved := showCostApprovalDialog(job.GetTotalEstimatedCost(), job.GetCurrency())
// 4. Confirm or cancel
if approved {
_, err = client.Jobs.Confirm(ctx, job.GetId())
} else {
_, err = client.Jobs.Cancel(ctx, job.GetId())
}
if err != nil {
log.Fatal(err)
}
}When to Use Delayed Jobs
| Scenario | Recommended |
|---|---|
| User-uploaded content with pay-per-use billing | Yes |
| Internal batch processing with fixed budgets | Yes |
| Automated pipelines with predictable inputs | No, use immediate start |
| Preview generation for quick feedback | No, use immediate start |
| High-value content requiring cost approval | Yes |