Delayed Jobs
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. The job is created immediately but pauses after probing instead of encoding right away — the example requests two outputs (a single MP4 plus a three-rung HLS ladder) so the estimate in the next step has something to price:
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 fills in the fields you need to make a decision. input_metadata reports what probing found — container format, duration_ms, and the source video and audio streams (codec, resolution, frame rate). total_estimated_cost and each output’s estimated_cost give the price, and for ABR outputs variant_pricing breaks that down per rung so you can see exactly what each resolution contributes:
{
"job": {
"id": "job_a1b2c3d4e5f6",
"status": "awaiting_confirmation",
"delayed_start": true,
"total_estimated_cost": 0.3455,
"currency": "EUR",
"input_metadata": {
"format": "mp4",
"duration_ms": 120000,
"size_bytes": 52428800,
"bit_rate": 3495000,
"video": {
"codec": "h264",
"width": 1920,
"height": 1080,
"frame_rate": 30,
"pixel_format": "yuv420p"
},
"audio": {
"codec": "aac",
"sample_rate": 48000,
"channels": 2,
"channel_layout": "stereo"
}
},
"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/sdk";
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")Canceling at awaiting_confirmation incurs no charges at all — probing itself is not billed, and no encoding has started. You are only charged for encoding that actually runs.
Implementation Example
Here is the whole flow end to end for a web application: create the job with delayed_start, poll until it reaches awaiting_confirmation, show the estimated cost to the user for approval, then confirm to start encoding or cancel to walk away — all four steps in one place:
import {
Transcodely,
JobStatus,
OutputFormat,
VideoCodec,
Resolution,
QualityTier,
} from "@transcodely/sdk";
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 |