Skip to content
Migration guide

Migrate from CloudConvert

switchfile accepts CloudConvert v2 jobs. Change the base URL and the API key, then check the few differences listed on this page. Webhooks are set per job with webhook_url. Webhooks for the whole account, set in the CloudConvert dashboard, do not exist at switchfile.

Three steps

  1. Step 1

    Create a key

    Sign up and create an API key under Dashboard > API keys. Start with a sandbox key: it works the same way and never uses credits.

  2. Step 2

    Change the base URL and the key

    Point your requests at the switchfile address and send the new key as a bearer token. Request bodies and responses keep their structure.

  3. Step 3

    Set webhooks per job

    Webhooks set for the whole account in the CloudConvert dashboard do not exist at switchfile. Add webhook_url to each job, swap the signing secret and check your options against the guide.

What to change

API base URLBefore: https://api.cloudconvert.com/v2After: https://api.switchfile.io/v2
Wait in the same requestBefore: https://sync.api.cloudconvert.com/v2After: https://api.switchfile.io/sync/v2
SandboxBefore: https://api.sandbox.cloudconvert.com/v2After: https://api.switchfile.io/v2 with a sandbox key
API keyBefore: CloudConvert dashboardAfter: Dashboard > API keys, live or sandbox
Webhook URLBefore: Dashboard or webhook_url on the jobAfter: webhook_url on the job
Webhook signatureBefore: CloudConvert-Signature headerAfter: Same header, secret from Dashboard > Webhooks
Format listBefore: https://api.cloudconvert.com/v2/convert/formatsAfter: https://api.switchfile.io/v2/convert/formats

The same job, new address

Import a file from a URL, convert it to MP4 and get a download link. The comments show the old value. Uploads through import/upload and result.form work the same way.

# was: https://api.cloudconvert.com/v2/jobs and your CloudConvert key. Needs jq.
RES=$(curl -sS https://api.switchfile.io/v2/jobs \
  -H "Authorization: Bearer $SWITCHFILE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": {
      "import": { "operation": "import/url", "url": "https://example.com/talk.mkv" },
      "convert": { "operation": "convert", "input": "import", "output_format": "mp4" },
      "export": { "operation": "export/url", "input": "convert" }
    }
  }')
# Errors come back as {"message", "code"} with the HTTP status, as at CloudConvert.
JOB_ID=$(echo "$RES" | jq -er .data.id) || { echo "$RES" | jq -r '"\(.code): \(.message)"' >&2; exit 1; }

# /wait answers when the job ends, or after a server-side timeout with the job still running.
RES=$(curl -sS --max-time 360 https://api.switchfile.io/v2/jobs/$JOB_ID/wait \
  -H "Authorization: Bearer $SWITCHFILE_API_KEY")
case $(echo "$RES" | jq -r .data.status) in
  finished) echo "$RES" | jq -r '.data.tasks[] | select(.name == "export") | .result.files[0].url' ;;
  error) echo "$RES" | jq -r '.data.tasks[] | select(.status == "error") | "\(.name) failed: \(.code) \(.message)"' >&2; exit 1 ;;
  waiting|processing) echo "Job $JOB_ID is still running: call /wait again or send webhook_url" >&2; exit 1 ;;
  *) echo "$RES" | jq -r '"\(.code): \(.message)"' >&2; exit 1 ;;
esac

/wait returns when the job ends or after a timeout. For long videos, send webhook_url with the job instead of waiting.

Webhooks

Webhooks are set per job with webhook_url. Webhooks for the whole account, set in the CloudConvert dashboard, do not exist at switchfile. The same goes for webhooks created through /v2/webhooks: add webhook_url to each job instead. We call webhook_url with job.finished or job.failed and the job in the body, signed with HMAC-SHA256 in the CloudConvert-Signature header. The signature check stays the same; only the secret changes.

import { createHmac, timingSafeEqual } from "node:crypto";

// Same header as before. Only the secret changes: copy it from Dashboard > Webhooks.
function isValid(rawBody, signature, secret = process.env.SWITCHFILE_WEBHOOK_SECRET) {
  const expected = Buffer.from(createHmac("sha256", secret).update(rawBody).digest("hex"));
  const given = Buffer.from(signature ?? "");
  // Compare byte lengths first: timingSafeEqual throws on buffers of different length.
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// isValid(rawBody, req.headers["cloudconvert-signature"])

Differences to check

From our own comparison of both APIs on 7 November 2026.

Webhooks are set per job

Send webhook_url with each job. The /v2/webhooks endpoint for account-wide webhooks is not available. Events are job.finished and job.failed, signed with the CloudConvert-Signature header.

TIFF output is called tif

Use output_format "tif". A job with output_format "tiff" is rejected with 422. As an input, both spellings work.

PDF to image renders at 150 dpi by default

Set pixel_density to 300 if you need the same page size in pixels as before.

Some options are not supported yet

We ignore options we do not know instead of rejecting the job. Today that includes watermark_image, subtitles, subtitles_mode, all_audio_streams, volume, channels, sample_rate, audio_qscale, keyframe_interval, aspect_ratio, profile, level, tune and qscale for audio and video, and pdf_a, password, optimize_print, include_markup and bookmarks for documents to PDF.

Default MP3 bitrate differs

Set audio_bitrate, for example 192, if your files need a specific bitrate. audio_qscale is not read.

Error texts differ, codes match

Status codes and the code field match CloudConvert. The errors object uses field paths such as tasks.c.input.

Format list without options

GET /v2/convert/formats has the same structure, but include=options does not return the options of each pair.

In the docs: jobs and tasks, waiting for a job, webhooks for jobs and the reference for POST /v2/jobs.

Compare before you switch

Prices and the formats CloudConvert has that we do not, each with its source.