Skip to content

Storage Providers (BYOB)

By default, Xora stores transcoding outputs in its own managed storage and gives you signed download URLs. With Bring Your Own Bucket (BYOB), you can route outputs directly to your Cloudflare R2 bucket.

Without BYOB: With BYOB:
Video → Xora → Xora storage Video → Xora → Your R2 bucket
↓ ↓
Signed URL (expires) Your bucket (permanent)

With BYOB, outputs land in your R2 bucket under a prefix you configure. You own the files and manage access yourself.

  1. Create an R2 bucket

    In your Cloudflare dashboard, create an R2 bucket. Note your Account ID (shown in the sidebar) and bucket name.

  2. Create R2 API credentials

    Go to R2 → Manage R2 API TokensCreate API Token:

    • Permission: Object Read & Write
    • Scope: your bucket
    • Copy the Access Key ID and Secret Access Key
  3. Add the storage provider in Xora

    In the Xora dashboard, go to Storage ProvidersAdd Provider, or use the API:

    Terminal window
    curl -X POST https://api.xora.sh/v1/settings/storage-providers \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "My R2 Bucket",
    "provider": "r2",
    "accountId": "your-cloudflare-account-id",
    "bucket": "my-xora-outputs",
    "prefix": "transcoded/",
    "accessKeyId": "your-r2-access-key",
    "secretAccessKey": "your-r2-secret-key",
    "makeDefault": true
    }'
  4. Test the connection

    Terminal window
    curl -X POST https://api.xora.sh/v1/settings/storage-providers/PROVIDER_ID/test \
    -H "Authorization: Bearer YOUR_API_KEY"
    { "ok": true }

The test writes a small file, verifies it exists, then deletes it. If something is wrong (bad credentials, wrong bucket name), you’ll get an error message explaining the issue.

FieldTypeRequiredDescription
namestringYesA label for this provider (e.g., “Production R2”)
providerstringYesMust be "r2"
accountIdstring*Your Cloudflare account ID
endpointstring*Custom S3-compatible endpoint URL
bucketstringYesR2 bucket name
prefixstringNoObject key prefix (e.g., "media/") — trailing slash added automatically
accessKeyIdstringYesR2 API token access key ID
secretAccessKeystringYesR2 API token secret access key
makeDefaultbooleanNoSet as the default storage destination

*Provide either accountId or endpoint. If you provide accountId, the endpoint is auto-generated as https://{accountId}.r2.cloudflarestorage.com.

When you set a provider as default (makeDefault: true), all new jobs automatically route their outputs to that bucket. You can change the default at any time:

Terminal window
curl -X PUT https://api.xora.sh/v1/settings \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"defaultStorageProviderId": "01KSTORAGE1234ABCDEF"
}'

Set to null to go back to Xora-hosted storage:

{ "defaultStorageProviderId": null }
Terminal window
curl https://api.xora.sh/v1/settings/storage-providers \
-H "Authorization: Bearer YOUR_API_KEY"
{
"providers": [
{
"id": "01KSTORAGE1234ABCDEF",
"name": "My R2 Bucket",
"type": "s3_compatible",
"provider": "r2",
"endpoint": "https://abc123.r2.cloudflarestorage.com",
"bucket": "my-xora-outputs",
"prefix": "transcoded/",
"status": "active",
"isDefault": true,
"createdAt": "2026-06-01T10:00:00.000Z"
}
],
"defaultStorageProviderId": "01KSTORAGE1234ABCDEF"
}
Terminal window
curl -X PUT https://api.xora.sh/v1/settings/storage-providers/PROVIDER_ID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Name",
"provider": "r2",
"accountId": "your-cloudflare-account-id",
"bucket": "new-bucket-name",
"accessKeyId": "your-r2-access-key"
}'
Terminal window
curl -X DELETE https://api.xora.sh/v1/settings/storage-providers/PROVIDER_ID \
-H "Authorization: Bearer YOUR_API_KEY"

If the deleted provider was your default, the default is automatically cleared (reverts to Xora-hosted storage).

StatusMeaning
pendingJust created, not yet tested
activeConnection test passed
errorLast test failed — check lastError for details

When creating a job, you can optionally override the default output path (outputs/<jobId>/result.<format>) inside your bucket using the outputPath parameter on the job creation request or in the dashboard New job form.

  • Custom storage default: This option is only supported when a default custom storage provider is configured.
  • Allowed characters: a-zA-Z0-9_-/. (alphanumeric, hyphens, underscores, dots, and forward slashes). No path traversal (../) or leading/trailing slashes are allowed.
  • Extension match: The extension in the outputPath must exactly match the job’s output format (e.g. path/to/my-video.mp4 for format mp4).
  • Scope restriction: Not supported on multi-file ffmpeg mode jobs.
Terminal window
curl -X POST https://api.xora.sh/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "recipe",
"input": { "url": "https://example.com/raw.mp4" },
"output": { "format": "mp4" },
"recipe": { "name": "compress" },
"outputPath": "processed/2026/june/final-compress.mp4"
}'

If your storage prefix is configured as transcoded/, the output will be saved at the key transcoded/processed/2026/june/final-compress.mp4 in your R2 bucket.

  • Your secretAccessKey is encrypted at rest using AES and never returned in API responses
  • Credentials are only decrypted at job time to upload outputs to your bucket
  • R2 API tokens should be scoped to the minimum required permission (Object Read & Write on a single bucket)