Search Documentation
Search across all documentation pages
Idempotency

Idempotency

Idempotency ensures that retrying a request produces the same result as the original request, without creating duplicate resources. This is critical for handling network failures, timeouts, and other transient errors in production systems.

How It Works

When creating a job, include an idempotency_key in the request. If Transcodely receives a second request with the same key, it returns the result of the original request instead of creating a new job.

curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Create 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: org_a1b2c3d4e5" 
  -H "Content-Type: application/json" 
  -d '{
    "input_url": "gs://my-bucket/video.mp4",
    "output_origin_id": "ori_x9y8z7w6v5",
    "outputs": [
      {
        "type": "mp4",
        "video": [
          { "codec": "h264", "resolution": "1080p", "quality": "standard" }
        ]
      }
    ],
    "idempotency_key": "upload_usr12345_2026-01-15T10:30:00Z"
  }'
const job = await client.jobs.create({
  inputUrl: "gs://my-bucket/video.mp4",
  outputOriginId: "ori_x9y8z7w6v5",
  outputs: [{
    type: OutputFormat.MP4,
    video: [
      { codec: VideoCodec.H264, resolution: Resolution.RESOLUTION_1080P, quality: QualityTier.STANDARD },
    ],
  }],
  idempotencyKey: "upload_usr12345_2026-01-15T10:30:00Z",
});
job = client.jobs.create(
    input_url="gs://my-bucket/video.mp4",
    output_origin_id="ori_x9y8z7w6v5",
    outputs=[{
        "type": "mp4",
        "video": [
            {"codec": "h264", "resolution": "1080p", "quality": "standard"}
        ],
    }],
    idempotency_key="upload_usr12345_2026-01-15T10:30:00Z",
)
job, err := client.Jobs.Create(ctx, &transcodely.JobCreateParams{
	InputUrl:       "gs://my-bucket/video.mp4",
	OutputOriginId: proto.String("ori_x9y8z7w6v5"),
	Outputs: []*transcodely.OutputSpec{{
		Type: transcodely.OutputFormatMP4,
		Video: []*transcodely.VideoVariant{
			{Codec: transcodely.VideoCodecH264, Resolution: transcodely.Resolution1080P, Quality: transcodely.QualityTierStandard},
		},
	}},
	IdempotencyKey: proto.String("upload_usr12345_2026-01-15T10:30:00Z"),
})

First Request

The job is created normally and the idempotency key is associated with the job.

Subsequent Requests (Same Key)

The API returns the existing job without creating a new one. The response is identical to what the first request returned.

SDKs auto-inject idempotency keys

The official SDKs generate a UUID v4 Idempotency-Key for every Create mutation automatically, so retrying within the same process is safe with no extra code. For cross-process safety (queue workers, cron jobs, retried RPCs), pass your own key — that survives a process restart whereas the auto-generated one does not.

curl -X POST https://api.transcodely.com/transcodely.v1.JobService/Create 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -H "Content-Type: application/json" 
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" 
  -d '{ "input_url": "...", "outputs": [] }'
const job = await client.jobs.create({
  inputUrl: "...",
  outputs: [/* ... */],
  idempotencyKey: `transcode_asset_${assetId}_v1`,
});
client.jobs.create(
    input_url="...",
    outputs=[...],
    idempotency_key=f"transcode_asset_{asset_id}_v1",
)
import "google.golang.org/protobuf/proto"

job, err := client.Jobs.Create(ctx, &transcodely.JobCreateParams{
    InputUrl:       "...",
    Outputs:        []*transcodely.OutputSpec{/* ... */},
    IdempotencyKey: proto.String("transcode_asset_42_v1"),
})

To disable auto-injection across the entire Go client (e.g. when targeting a server that doesn’t yet support idempotency keys), use transcodely.WithAutoIdempotency(false) at construction. The TypeScript and Python SDKs always auto-inject — pass an explicit key to override.

Key Format

Idempotency keys are free-form strings with a maximum length of 128 characters. We recommend using a format that ties the key to the specific operation:

StrategyExampleBest For
UUID v4550e8400-e29b-41d4-a716-446655440000Simple, guaranteed uniqueness
Operation-basedupload_usr12345_2026-01-15T10:30:00ZReadable, debuggable
Content hashsha256:a1b2c3d4e5f6...Deduplication based on input
{action}_{entity_id}_{timestamp}

Examples:

  • transcode_vid_abc123_2026-01-15T10:30:00Z — ties to a specific upload
  • batch_campaign_summer2026_chunk_42 — ties to a specific batch item
  • retry_job_a1b2c3_attempt_3 — explicit retry tracking

Scope

Idempotency keys are scoped to the app associated with the API key. The same key can be used independently across different apps without conflict.

Behavior on Replay

ScenarioBehavior
Same key, same request bodyReturns the original job
Same key, different request bodyReturns the original job (request body is not compared)
Same key, different API key (same app)Returns the original job
Same key, different appCreates a new job (keys are app-scoped)

Important: The API does not compare request bodies when an idempotency key is replayed. If you reuse a key with a different request body, you will get back the original job — not a new job with the new parameters. Always use unique keys for distinct operations.

Expiration

Idempotency keys are stored for 24 hours. After 24 hours, a previously used key can be reused to create a new job.

When to Use Idempotency Keys

  • Network retries — your HTTP client automatically retries on timeout or connection reset
  • Queue-based processing — a message queue may deliver the same message more than once
  • User-triggered actions — a user clicks “Submit” multiple times before the UI disables the button
  • Batch processing — processing a list of items where some may need to be retried

Example: Safe Retry Logic

The loops below are written for direct callers (raw Connect-RPC stubs). If you use one of the official SDKs, you get retry-with-jittered-backoff and auto-idempotency for free — see the SDK auto-injection section above.

key="job_$(uuidgen)"
for attempt in 1 2 3 4; do
  curl -sf -X POST https://api.transcodely.com/transcodely.v1.JobService/Create 
    -H "Authorization: Bearer {{API_KEY}}" 
    -H "X-Organization-ID: {{ORG_ID}}" 
    -H "Content-Type: application/json" 
    -H "Idempotency-Key: $key" 
    -d '{
      "input_url": "gs://my-bucket/video.mp4",
      "output_origin_id": "ori_x9y8z7w6v5",
      "outputs": [{ "type": "mp4", "video": [{ "codec": "h264", "resolution": "1080p" }] }]
    }' && break
  sleep $((2 ** (attempt - 1)))
done
async function createJobWithRetry(
  client: JobServiceClient,
  request: CreateJobRequest,
  maxRetries = 3
): Promise<Job> {
  // Generate a unique idempotency key for this operation
  const idempotencyKey = `job_${crypto.randomUUID()}`;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await client.create({
        ...request,
        idempotency_key: idempotencyKey,
      });
      return response.job;
    } catch (err) {
      if (err instanceof ConnectError) {
        // Don't retry on client errors
        if (err.code === Code.InvalidArgument || err.code === Code.NotFound) {
          throw err;
        }
        // Retry on transient errors
        if (attempt < maxRetries) {
          await sleep(Math.pow(2, attempt) * 1000); // Exponential backoff
          continue;
        }
      }
      throw err;
    }
  }
  throw new Error('Max retries exceeded');
}
import time
import uuid
from connectrpc.exceptions import ConnectError

def create_job_with_retry(client, request, max_retries=3):
    idempotency_key = f"job_{uuid.uuid4()}"

    for attempt in range(max_retries + 1):
        try:
            request.idempotency_key = idempotency_key
            response = client.create(request)
            return response.job
        except ConnectError as e:
            if e.code in ("invalid_argument", "not_found"):
                raise  # Don't retry client errors
            if attempt < max_retries:
                time.sleep(2 ** attempt)  # Exponential backoff
                continue
            raise
func createJobWithRetry(ctx context.Context, client *transcodely.Client, params *transcodely.JobCreateParams, maxRetries int) (*transcodely.Job, error) {
	// Generate a unique idempotency key for this operation
	params.IdempotencyKey = proto.String(fmt.Sprintf("job_%s", uuid.NewString()))

	var err error
	for attempt := 0; attempt <= maxRetries; attempt++ {
		var job *transcodely.Job
		job, err = client.Jobs.Create(ctx, params)
		if err == nil {
			return job, nil
		}
		// Don't retry on client errors
		var invalid *transcodely.InvalidRequestError
		var notFound *transcodely.NotFoundError
		if errors.As(err, &invalid) || errors.As(err, &notFound) {
			return nil, err
		}
		// Retry on transient errors with exponential backoff
		if attempt < maxRetries {
			time.Sleep(time.Duration(1<<attempt) * time.Second)
		}
	}
	return nil, err
}

Best Practices

  1. Generate the key before the first attempt and reuse it across retries.
  2. Use descriptive, deterministic keys when possible — they make debugging easier.
  3. Never reuse a key for a different operation — always generate a new key for each distinct request.
  4. Store the key alongside your internal records so you can trace which Transcodely job maps to which internal entity.