Search Documentation
Search across all documentation pages
Storage Setup

Overview

Before creating transcoding jobs, you need to configure origins — storage locations where Transcodely reads input files and writes transcoded outputs. Transcodely supports Google Cloud Storage (GCS), Amazon S3, Cloudflare R2, and HTTP URLs as origin providers.

Origins are scoped to your app and can have read permission (for inputs), write permission (for outputs), or both. Credentials are validated at creation time.


Google Cloud Storage

1. Create a Service Account

In the Google Cloud Console, create a dedicated service account for Transcodely. Avoid reusing existing accounts — a purpose-built account makes it easy to audit and revoke access.

gcloud iam service-accounts create transcodely-storage 
  --display-name="Transcodely Storage Access" 
  --project=your-project-id

2. Grant Bucket Permissions

Assign the minimum required roles. For an origin that both reads inputs and writes outputs, you need storage.objectViewer and storage.objectCreator:

{
  "bindings": [
    {
      "role": "roles/storage.objectViewer",
      "members": [
        "serviceAccount:transcodely-storage@your-project-id.iam.gserviceaccount.com"
      ]
    },
    {
      "role": "roles/storage.objectCreator",
      "members": [
        "serviceAccount:transcodely-storage@your-project-id.iam.gserviceaccount.com"
      ]
    }
  ]
}

Apply the policy to your bucket:

gcloud storage buckets add-iam-policy-binding gs://your-video-bucket 
  --member="serviceAccount:transcodely-storage@your-project-id.iam.gserviceaccount.com" 
  --role="roles/storage.objectViewer"

gcloud storage buckets add-iam-policy-binding gs://your-video-bucket 
  --member="serviceAccount:transcodely-storage@your-project-id.iam.gserviceaccount.com" 
  --role="roles/storage.objectCreator"

For a read-only input origin, only storage.objectViewer is needed. For a write-only output origin, only storage.objectCreator is needed.

3. Generate a Service Account Key

gcloud iam service-accounts keys create sa-key.json 
  --iam-account=transcodely-storage@your-project-id.iam.gserviceaccount.com

4. Create the Origin

curl -X POST https://api.transcodely.com/transcodely.v1.OriginService/Create 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -d '{
    "name": "Production GCS Bucket",
    "description": "Primary storage for video inputs and outputs",
    "permissions": ["read", "write"],
    "base_path": "videos/",
    "path_template": "{date}/{job_id}/{codec}_{resolution}",
    "gcs": {
      "bucket": "your-video-bucket",
      "credentials": {
        "service_account_json": "<contents of sa-key.json>"
      }
    }
  }'
const origin = await client.origins.create({
  name: "Production GCS Bucket",
  description: "Primary storage for video inputs and outputs",
  permissions: [OriginPermission.READ, OriginPermission.WRITE],
  basePath: "videos/",
  pathTemplate: "{date}/{job_id}/{codec}_{resolution}",
  gcs: {
    bucket: "your-video-bucket",
    credentials: {
      serviceAccountJson: "<contents of sa-key.json>",
    },
  },
});
origin = client.origins.create(
    name="Production GCS Bucket",
    description="Primary storage for video inputs and outputs",
    permissions=["read", "write"],
    base_path="videos/",
    path_template="{date}/{job_id}/{codec}_{resolution}",
    gcs={
        "bucket": "your-video-bucket",
        "credentials": {
            "service_account_json": "<contents of sa-key.json>",
        },
    },
)
origin, err := client.Origins.Create(ctx, &transcodely.OriginCreateParams{
	Name:         "Production GCS Bucket",
	Description:  "Primary storage for video inputs and outputs",
	Permissions:  []transcodely.OriginPermission{transcodely.OriginPermissionRead, transcodely.OriginPermissionWrite},
	BasePath:     "videos/",
	PathTemplate: "{date}/{job_id}/{codec}_{resolution}",
	Gcs: &transcodely.GcsOriginConfig{
		Bucket: "your-video-bucket",
		Credentials: &transcodely.GcsCredentials{
			ServiceAccountJson: "<contents of sa-key.json>",
		},
	},
})

The response includes a validation object confirming Transcodely was able to read from and write to the bucket:

{
  "origin": {
    "id": "ori_x9y8z7w6v5",
    "name": "Production GCS Bucket",
    "provider": "gcs",
    "status": "active",
    "permissions": ["read", "write"]
  },
  "validation": {
    "success": true,
    "can_read": true,
    "can_write": true
  }
}

Amazon S3

1. Create an IAM User

Create a dedicated IAM user for Transcodely in the AWS Console or CLI:

aws iam create-user --user-name transcodely-storage

2. Attach a Bucket Policy

Create an IAM policy with the minimum permissions. For a read/write origin:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TranscodelyReadAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::your-video-bucket",
        "arn:aws:s3:::your-video-bucket/*"
      ]
    },
    {
      "Sid": "TranscodelyWriteAccess",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl"
      ],
      "Resource": [
        "arn:aws:s3:::your-video-bucket/*"
      ]
    }
  ]
}

Attach the policy to the IAM user:

aws iam put-user-policy 
  --user-name transcodely-storage 
  --policy-name TranscodelyStorageAccess 
  --policy-document file://transcodely-policy.json

3. Generate Access Keys

aws iam create-access-key --user-name transcodely-storage

Save the AccessKeyId and SecretAccessKey from the response. The secret is shown only once.

4. Create the Origin

curl -X POST https://api.transcodely.com/transcodely.v1.OriginService/Create 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -d '{
    "name": "Production S3 Bucket",
    "description": "US East video storage",
    "permissions": ["read", "write"],
    "base_path": "videos/",
    "path_template": "{date}/{job_id}/{codec}_{resolution}",
    "s3": {
      "bucket": "your-video-bucket",
      "region": "us-east-1",
      "credentials": {
        "access_key_id": "AKIAIOSFODNN7EXAMPLE",
        "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
      }
    }
  }'
const origin = await client.origins.create({
  name: "Production S3 Bucket",
  description: "US East video storage",
  permissions: [OriginPermission.READ, OriginPermission.WRITE],
  basePath: "videos/",
  pathTemplate: "{date}/{job_id}/{codec}_{resolution}",
  s3: {
    bucket: "your-video-bucket",
    region: "us-east-1",
    credentials: {
      accessKeyId: "AKIAIOSFODNN7EXAMPLE",
      secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    },
  },
});
origin = client.origins.create(
    name="Production S3 Bucket",
    description="US East video storage",
    permissions=["read", "write"],
    base_path="videos/",
    path_template="{date}/{job_id}/{codec}_{resolution}",
    s3={
        "bucket": "your-video-bucket",
        "region": "us-east-1",
        "credentials": {
            "access_key_id": "AKIAIOSFODNN7EXAMPLE",
            "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        },
    },
)
origin, err := client.Origins.Create(ctx, &transcodely.OriginCreateParams{
	Name:         "Production S3 Bucket",
	Description:  "US East video storage",
	Permissions:  []transcodely.OriginPermission{transcodely.OriginPermissionRead, transcodely.OriginPermissionWrite},
	BasePath:     "videos/",
	PathTemplate: "{date}/{job_id}/{codec}_{resolution}",
	S3: &transcodely.S3OriginConfig{
		Bucket: "your-video-bucket",
		Region: "us-east-1",
		Credentials: &transcodely.S3Credentials{
			AccessKeyId:     "AKIAIOSFODNN7EXAMPLE",
			SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
		},
	},
})

Cloudflare R2

Cloudflare R2 is an S3-compatible object store with zero egress fees, which makes it a strong fit for output origins.

1. Create an R2 API Token

In the Cloudflare dashboard, go to R2 → Manage R2 API Tokens and create a new token. Scope it to Object Read & Write if the origin will both read inputs and write outputs, or Object Read for input-only origins. Cloudflare returns an Access Key ID and Secret Access Key — copy both immediately, the secret is shown only once.

2. Find Your Account ID

In the Cloudflare dashboard, open R2 → Overview. Your account ID is a 32-character lowercase hex string shown in the right-hand panel. Transcodely uses this to derive the R2 endpoint server-side, so you don’t have to remember the bucket’s hostname:

JurisdictionDerived endpoint
default<account_id>.r2.cloudflarestorage.com
eu<account_id>.eu.r2.cloudflarestorage.com
fedramp<account_id>.fedramp.r2.cloudflarestorage.com

3. Create the Origin

The recommended form supplies account_id and lets us derive the endpoint:

curl -X POST https://api.transcodely.com/transcodely.v1.OriginService/Create 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -d '{
    "name": "Production R2 Bucket",
    "description": "Cloudflare R2 outputs",
    "permissions": ["read", "write"],
    "base_path": "videos/",
    "path_template": "{date}/{job_id}/{codec}_{resolution}",
    "r2": {
      "bucket": "your-video-bucket",
      "account_id": "f037e1abcd0987654321fedcba012345",
      "credentials": {
        "access_key_id": "...",
        "secret_access_key": "..."
      }
    }
  }'
const origin = await client.origins.create({
  name: "Production R2 Bucket",
  description: "Cloudflare R2 outputs",
  permissions: [OriginPermission.READ, OriginPermission.WRITE],
  basePath: "videos/",
  pathTemplate: "{date}/{job_id}/{codec}_{resolution}",
  r2: {
    bucket: "your-video-bucket",
    accountId: "f037e1abcd0987654321fedcba012345",
    credentials: {
      accessKeyId: "...",
      secretAccessKey: "...",
    },
  },
});
origin = client.origins.create(
    name="Production R2 Bucket",
    description="Cloudflare R2 outputs",
    permissions=["read", "write"],
    base_path="videos/",
    path_template="{date}/{job_id}/{codec}_{resolution}",
    r2={
        "bucket": "your-video-bucket",
        "account_id": "f037e1abcd0987654321fedcba012345",
        "credentials": {
            "access_key_id": "...",
            "secret_access_key": "...",
        },
    },
)
origin, err := client.Origins.Create(ctx, &transcodely.OriginCreateParams{
	Name:         "Production R2 Bucket",
	Description:  "Cloudflare R2 outputs",
	Permissions:  []transcodely.OriginPermission{transcodely.OriginPermissionRead, transcodely.OriginPermissionWrite},
	BasePath:     "videos/",
	PathTemplate: "{date}/{job_id}/{codec}_{resolution}",
	R2: &transcodely.R2OriginConfig{
		Bucket:    "your-video-bucket",
		AccountId: "f037e1abcd0987654321fedcba012345",
		Credentials: &transcodely.S3Credentials{
			AccessKeyId:     "...",
			SecretAccessKey: "...",
		},
	},
})

For data residency, set jurisdiction to "eu" or "fedramp":

"r2": {
  "bucket": "your-video-bucket",
  "account_id": "f037e1abcd0987654321fedcba012345",
  "jurisdiction": "eu",
  "credentials": { "access_key_id": "...", "secret_access_key": "..." }
}

For custom domains or future jurisdictions, supply an explicit endpoint instead of account_id. Exactly one of account_id or endpoint must be set.

"r2": {
  "bucket": "your-video-bucket",
  "endpoint": "https://media.example.com",
  "credentials": { "access_key_id": "...", "secret_access_key": "..." }
}

R2 access keys can be rotated via the Update endpoint with r2_credentials. The bucket, account ID, jurisdiction, and endpoint are immutable — archive the origin and create a new one to change them.


Separate Input and Output Origins

A common pattern is to use separate origins for inputs and outputs, with different buckets and permissions:

# Input origin (read-only)
curl -X POST https://api.transcodely.com/transcodely.v1.OriginService/Create 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -d '{
    "name": "Upload Bucket (Input)",
    "permissions": ["read"],
    "gcs": {
      "bucket": "my-uploads-bucket",
      "credentials": {
        "service_account_json": "<sa-key contents>"
      }
    }
  }'

# Output origin (write-only)
curl -X POST https://api.transcodely.com/transcodely.v1.OriginService/Create 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -d '{
    "name": "CDN Bucket (Output)",
    "permissions": ["write"],
    "path_template": "{date}/{job_id}/{resolution}",
    "gcs": {
      "bucket": "my-cdn-bucket",
      "credentials": {
        "service_account_json": "<sa-key contents>"
      }
    }
  }'
// Input origin (read-only)
const inputOrigin = await client.origins.create({
  name: "Upload Bucket (Input)",
  permissions: [OriginPermission.READ],
  gcs: {
    bucket: "my-uploads-bucket",
    credentials: {
      serviceAccountJson: "<sa-key contents>",
    },
  },
});

// Output origin (write-only)
const outputOrigin = await client.origins.create({
  name: "CDN Bucket (Output)",
  permissions: [OriginPermission.WRITE],
  pathTemplate: "{date}/{job_id}/{resolution}",
  gcs: {
    bucket: "my-cdn-bucket",
    credentials: {
      serviceAccountJson: "<sa-key contents>",
    },
  },
});
# Input origin (read-only)
input_origin = client.origins.create(
    name="Upload Bucket (Input)",
    permissions=["read"],
    gcs={
        "bucket": "my-uploads-bucket",
        "credentials": {
            "service_account_json": "<sa-key contents>",
        },
    },
)

# Output origin (write-only)
output_origin = client.origins.create(
    name="CDN Bucket (Output)",
    permissions=["write"],
    path_template="{date}/{job_id}/{resolution}",
    gcs={
        "bucket": "my-cdn-bucket",
        "credentials": {
            "service_account_json": "<sa-key contents>",
        },
    },
)
// Input origin (read-only)
inputOrigin, err := client.Origins.Create(ctx, &transcodely.OriginCreateParams{
	Name:        "Upload Bucket (Input)",
	Permissions: []transcodely.OriginPermission{transcodely.OriginPermissionRead},
	Gcs: &transcodely.GcsOriginConfig{
		Bucket: "my-uploads-bucket",
		Credentials: &transcodely.GcsCredentials{
			ServiceAccountJson: "<sa-key contents>",
		},
	},
})

// Output origin (write-only)
outputOrigin, err := client.Origins.Create(ctx, &transcodely.OriginCreateParams{
	Name:         "CDN Bucket (Output)",
	Permissions:  []transcodely.OriginPermission{transcodely.OriginPermissionWrite},
	PathTemplate: "{date}/{job_id}/{resolution}",
	Gcs: &transcodely.GcsOriginConfig{
		Bucket: "my-cdn-bucket",
		Credentials: &transcodely.GcsCredentials{
			ServiceAccountJson: "<sa-key contents>",
		},
	},
})

Then reference both origins when creating a job:

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",
    "outputs": [
      {
        "type": "mp4",
        "video": [{ "codec": "h264", "resolution": "1080p", "quality": "standard" }]
      }
    ]
  }'
const job = await client.jobs.create({
  inputOriginId: "ori_input12345",
  inputPath: "uploads/my-video.mp4",
  outputOriginId: "ori_output6789",
  outputs: [
    {
      type: OutputFormat.MP4,
      video: [
        {
          codec: VideoCodec.H264,
          resolution: Resolution.RESOLUTION_1080P,
          quality: QualityTier.STANDARD,
        },
      ],
    },
  ],
});
job = client.jobs.create(
    input_origin_id="ori_input12345",
    input_path="uploads/my-video.mp4",
    output_origin_id="ori_output6789",
    outputs=[{
        "type": "mp4",
        "video": [{"codec": "h264", "resolution": "1080p", "quality": "standard"}],
    }],
)
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"),
	Outputs: []*transcodely.OutputSpec{{
		Type: transcodely.OutputFormatMP4,
		Video: []*transcodely.VideoVariant{{
			Codec:      transcodely.VideoCodecH264,
			Resolution: transcodely.Resolution1080P,
			Quality:    transcodely.QualityTierStandard,
		}},
	}},
})

Re-validating Credentials

If you rotate credentials or change bucket permissions, re-validate an existing origin:

curl -X POST https://api.transcodely.com/transcodely.v1.OriginService/Validate 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "X-Organization-ID: {{ORG_ID}}" 
  -d '{ "id": "ori_x9y8z7w6v5" }'
const result = await client.origins.validate({ id: "ori_x9y8z7w6v5" });
console.log(result.validation?.success, result.validation?.canWrite);
result = client.origins.validate(id="ori_x9y8z7w6v5")
print(result.validation.success, result.validation.can_write)
validation, err := client.Origins.Validate(ctx, "ori_x9y8z7w6v5")
fmt.Println(validation.GetSuccess(), validation.GetCanWrite())

If validation fails, the origin status changes to failed and it cannot be used for new jobs until the issue is resolved and validation succeeds again.


Best Practices

PracticeRationale
Use dedicated service accounts/IAM usersEasy to audit, rotate, and revoke without affecting other services
Apply least-privilege permissionsInput origins only need read; output origins only need write
Set base_path for organizationKeeps Transcodely files in a predictable directory structure
Use path_template on output originsAvoids path collisions and creates a clean file hierarchy
Re-validate after credential rotationCatches permission issues before they cause job failures